Testing MVC controllers
You can test the logic of your MVC controllers that work with content items, such as articles or products, info objects as well as any other objects. This is done by providing fake representations of these objects. Using this approach, you can create the necessary objects without accessing the database.
The recommended approach for providing fake objects is to use repositories (adapters) that, for content items, make use of generated classes. Repositories provide the following advantages over using DocumentQuery calls directly in controllers:
- Your tests will work regardless of the used methods. This is important as not all document query methods are supported when creating fake objects.
- Repositories only need to contain the methods the application really needs. This allows you to make changes to the implementation in a much smaller scale than if you were using document query.
We also recommend writing individual controllers to use dependency injection and retrieve their dependencies via their constructors. This approach allows you to create classes with clearly defined responsibilities, which are, in turn easier to test.
You can also use containers such as Autofac (or similar container implementations) to simplify the process of creating controllers and their dependencies, as well as handling the life cycle of the dependencies.
See also: Initializing Xperience services with dependency injection
See the project code of the Dancing Goat MVC sample site for a reference on how to implement tests for your controllers.
Providing content items in controller tests
The following example demonstrates the implementation of a sample ArticleController controller and corresponding ArticleControllerTests. The controller displays article details. Note that the sample implementation is dependent on external libraries, such as Autofac, TestStack.FluentMVCTesting, and NSubstitute.
Create a repository that provides access to content items using generated classes. The repository only needs to contain the methods required by the application, not the data layer.
public interface IArticleRepository { IEnumerable<Article> GetArticle(int documentId); ... }
public sealed class ArticleRepository : IArticleRepository ... public Article GetArticle(int documentId) { return new DocumentQuery<Article>() .WithId(documentId) .Culture(cultureCode) .OnSite(siteName) .FirstOrDefault(); } ...
Register the repository in your dependency injection provider or container. For example, in the application’s Startup.cs or Global.asax.cs file.
builder.Register<ArticleRepository>().As<IArticleRepository>();
Inject the repository and any other dependencies in the controller constructor.
public class ArticleController : Controller { private readonly IArticleRepository articleRepository; public ArticleController(IArticleRepository repository) { articleRepository = repository; } ...
The controller contains an action that returns a view based on a specified article’s ID, or HttpNotFoundResult if the article does not exist.
// GET: Articles/Show/{id} public ActionResult Show(int articleId) { var article = articleRepository.GetArticle(articleId); if (article == null) { return HttpNotFound(); } return View(article); }
Set up the controller test and create the controller with the required dependencies. The following example uses the NUnit testing framework.
private IArticleRepository articleRepository; private ArticleController controller; [SetUp] public void SetUp() { // Allows creating of articles without accessing the database Fake().DocumentType<Article>(Article.CLASS_NAME); // Creates a mock article repository var article = TreeNode.New<Article>() .With(a => a.Fields.Title = "TestArticle") .With(a => a.SetValue("DocumentID", 1)); articleRepository = Substitute.For<IArticleRepository>(); controller = new ArticleController(articleRepository); articleRepository.GetArticle(1).Returns(article); }
Create individual test cases for the controller.
[Test] public void Show_WithoutExistingArticle_ReturnsHttpNotFoundStatusCode() { controller.WithCallTo(c => c.Show(2)) .ShouldGiveHttpStatus(HttpStatusCode.NotFound); }