ASP. net mvc 4-Test-driven ASP. NET MVC

Source: Internet
Author: User
Document directory
  • Test-driven ASP. NET MVC
Test-driven ASP. NET MVC

Keith Burnell

Download Sample Code

The core of Model-View-controller (MVC) mode is to divide UI functions into three components. The model represents the data and behavior of your domain. View Management Model display and interaction with users. The Controller coordinates the interaction between views and models. In this way, the UI logic that is hard to be tested is separated from the business logic, making it easy to test applications implemented in MVC mode. In this article, I will discuss how to enhance your ASP. net mvc application testability best practices and technologies, including how to build the structure of your solution, design the code architecture to handle dependency injection, and use StructureMap to implement dependency injection.

Build the structure of your solution to achieve the highest Testability

There is no better way to start our discussion than starting a new project (that is, creating a solution) for every developer. Based on my experience in developing large enterprise ASP. net mvc applications using test-driven development (TDD), I will discuss some best practices for planning your Visual Studio solution. First, we recommend that you use an empty project template when creating an ASP. net mvc project. Other templates are suitable for testing or creating proof of concept, but they usually contain a lot of disturbing content that is distracting and unnecessary in real enterprise applications.

You should use the n-layer method when creating any type of complex applications. For ASP. net mvc application development, we recommend that youFigure 1AndFigure 2Contains the following items:

  • A Web project contains all UI-specific code, including views, view models, scripts, and CSS. This layer can only access Controllers, Service, Domain, and Shared projects.
  • The Controllers project contains the Controller class used by ASP. net mvc. This layer communicates with services, domains, and Shared projects.
  • The Service project contains the business logic of the application. This layer communicates with DataAccess, Domain, and Shared projects.
  • The DataAccess project contains the code used to retrieve and operate the data of the driver application. This layer communicates with the Domain and Shared projects.
  • The Domain project contains the Domain project used by the application and prohibits communication with any project.
  • A Shared project contains code that can be used for multiple other layers, such as record programs, constants, and other common utility code. Only allow this project to communicate with the Domain project.


Figure 1 interaction between layers


Figure 2 solution structure example

We recommend that you place your Controller in a separate Visual Studio project. For more information about how to easily implement this suggestion, see the blog post on bit. ly/K4mF2B. By placing your Controller in a separate project, you can further separate the logic in the controller from the UI code. The result is that your Web project only contains the real UI-related code.

Where to place your test projectIt is important to place your test projects and name them. When you develop complex and enterprise-level applications, solutions tend to become quite large. Therefore, it is difficult to locate specific classes or parts of the Code in Solution Explorer. Adding multiple test projects to your existing code library makes navigation more complex in Solution Explorer. I strongly recommend that you physically separate the test project from the actual application code. I suggest placing all test projects in the solution-level Tests folder. Locating All your test projects and tests in a single solution folder significantly reduces interference in the default Solution Explorer view, allowing you to easily locate your test.

Next, you will detach the test type. Your solution may include multiple Test types (unit, integration, performance, UI, etc.). Therefore, it is important to isolate and group each test type. This not only facilitates locating specific test types, but also allows you to easily run all tests of a specific type. If you use one of the most popular Visual Studio efficient tool kits ReSharper (jetbrains.com/ReSharper) or CodeRush (devexpress.com/CodeRush), you will get a context menu, this menu allows you to right-click any folder, project, or class in Solution Explorer and run all tests contained in this item. To group Tests by test type, create a folder for each test type you plan to write in the Tests solution folder.

Figure 3Shows an example of a Tests solution folder, which contains multiple test-type folders.


Figure 3 Example of the Tests solution folder

Name your test projectThe naming method of the test project is as important as that of the test project. You want to easily classify the applications to be tested in each test project and the Test types included in the project. Therefore, it is best to use the following Conventions to name your Test project: [full name of the project to be tested]. Test. [Test type]. This allows you to quickly and accurately determine the layer of the project to be tested and the type of the test to be executed. You may think that it is unnecessary to place a test project in a type-specific folder and include the test Type in the name of the test project, but remember, the solution folder is only used in Solution Explorer and is not included in the namespace of the project file. Therefore, although the Controllers Unit Test project is located in the Tests \ Unit solution folder, The namespace (TestDrivingMVC. Controllers. Test. Unit) does not reflect the folder structure. It is necessary to add a test type when naming a project, to avoid naming conflicts and to determine the test type that you process in the editor.Figure 4Displays the solution resource manager with a test project.


Figure 4 test project in Solution Explorer

Introduce dependency injection for your architecture

Before you encounter dependencies in your code to be tested, unit tests on n-layer applications will not go far. These dependencies can be other layers of your application, or they can be completely external to your code (such as databases, file systems, or Web services ). When you write a unit Test, you need to properly handle this situation and use Test Double (simulate, virtual, or stub) in case of external dependencies ). For more information about Test Double, see "explore the status set of Test Double" (msdn.microsoft.com/magazine/cc163358) published in MSDN September 2007 ). However, before you can use the flexibility provided by Test Double, you must design your code to handle dependency injection.

Dependency InjectionDependency injection is the process of injecting the specific implementation required by a Class (instead of directly instantiating the class of the dependency. Classes do not know the actual implementation of any dependencies, but only the interfaces that support dependencies. The specific implementation is provided by the class or dependency injection framework.

The purpose of dependency injection is to create code with a high degree of loose coupling. With loose coupling, you can easily replace the Test Double Implementation of your dependencies when writing unit tests.

There are three main methods for dependency injection:

  • Property Injection
  • Constructor Injection
  • Use dependency injection framework/control container reversal (referred to as DI/IoC framework since then)

Using property injection, you can publish public attributes of an object so that you can set its dependency, as shown in figureFigure 5. This method is simple and clear and does not require tools.

Figure 5 Property Injection


  
  
  1. // Employee Service
  2. public class EmployeeService : IEmployeeService {
  3. private ILoggingService _loggingService;
  4. public EmployeeService() {}
  5. public ILoggingService LoggingService { get; set; }
  6. public decimal CalculateSalary(long employeeId) {
  7. EnsureDependenciesSatisfied();
  8. _loggingService.LogDebug(string.Format(
  9. "Calculating Salary For Employee: {0}", employeeId));
  10. decimal output = 0;
  11. /*
  12. * Complex logic that needs to be performed
  13. * in order to determine the employee's salary
  14. */
  15. return output;
  16. }
  17. private void EnsureDependenciesSatisfied() {
  18. if (_loggingService == null)
  19. throw new InvalidOperationException(
  20. "Logging Service dependency must be satisfied!");
  21. }
  22. }
  23. }
  24. // Employee Controller (Consumer of Employee Service)
  25. public class EmployeeController : Controller {
  26. public ActionResult DisplaySalary(long id) {
  27. EmployeeService employeeService = new EmployeeService();
  28. employeeService.LoggingService = new LoggingService();
  29. decimal salary = employeeService.CalculateSalary(id);
  30. return View(salary);
  31. }
  32. }

This method has three disadvantages. First, it allows users to provide dependencies. Second, it requires you to implement code protection in the object to ensure that dependencies are set before use. Finally, as the number of dependencies on your object increases, the amount of code required to instantiate the object also increases.

Using constructor injection to implement dependency injection involves providing dependency to a class through its constructor when instantiating the constructor, as shown in figureFigure 6. This method is also simple and clear, but unlike property injection, you can always set the dependency of this class.

Figure 6 constructor Injection


  
  
  1. // Employee Service
  2. public class EmployeeService : IEmployeeService {
  3. private ILoggingService _loggingService;
  4. public EmployeeService(ILoggingService loggingService) {
  5. _loggingService = loggingService;
  6. }
  7. public decimal CalculateSalary(long employeeId) {
  8. _loggingService.LogDebug(string.Format(
  9. "Calculating Salary For Employee: {0}", employeeId));
  10. decimal output = 0;
  11. /*
  12. * Complex logic that needs to be performed
  13. * in order to determine the employee's salary
  14. */
  15. return output;
  16. }
  17. }
  18. // Consumer of Employee Service
  19. public class EmployeeController : Controller {
  20. public ActionResult DisplaySalary(long employeeId) {
  21. EmployeeService employeeService =
  22. new EmployeeService(new LoggingService());
  23. decimal salary = employeeService.CalculateSalary(employeeId);
  24. return View(salary);
  25. }
  26. }

Unfortunately, this method still requires users to provide dependencies. In addition, it is indeed only applicable to small applications. Large applications usually have too many dependencies, so they cannot be provided through object constructors.

The third method to implement dependency injection is to use the DI/IoC framework. The DI/IoC framework completely eliminates the user's responsibility for providing dependencies, and allows you to configure dependencies during design and parse dependencies at runtime. There are many DI/IoC frameworks available for. NET, including Unity (Microsoft products), StructureMap, Castle Windsor and Ninject. The basic concepts behind all different DI/IoC frameworks are the same, and the framework to choose is usually determined by personal preferences. To demonstrate the DI/IoC framework in this article, I will use StructureMap.

Use StructureMap to inject dependency to a higher level

StructureMap (structuremap.net) is a widely used dependency injection framework. You can use the Package Manager Console (Install-Package StructureMap) or NuGet Package Manager GUI (right-click the reference folder of your project and select "manage NuGet packages ") install the framework through NuGet.

Use StructureMap to configure DependenciesThe first step to implement StructureMap in ASP. net mvc is to configure your dependencies so that StructureMap knows how to parse them. You can configure the dependency in the Application_Start method of Global. asax in either of the following methods.

The first method is to manually indicate StructureMap. For specific abstract implementations, it should use specific implementations:


  
  
  1. ObjectFactory.Initialize(register => {
  2. register.For<ILoggingService>().Use<LoggingService>();
  3. register.For<IEmployeeService>().Use<EmployeeService>();
  4. });

The disadvantage of this method is that you must manually register every dependency in your application. Therefore, for large applications, the workload may be high. In addition, because you register dependencies in Application_Start of the ASP. net mvc site, your Web layer must directly know each other layer of the application bound with dependencies.

You can also use StructureMap to automatically register and scan your programs and bind dependencies. In this method, StructureMap scans your assembly and finds the specific implementation of the association when it encounters an interface (based on a concept, that is, according to the Convention, the method named IFoo will be mapped to the specific implement Foo ):


  
  
  1. ObjectFactory.Initialize(registry => registry.Scan(x => {
  2. x.AssembliesFromApplicationBaseDirectory();
  3. x.WithDefaultConventions();
  4. }));

StructureMap dependency SolutionAfter configuring your dependencies, you must be able to access these Dependencies from your code library. This is achieved by creating a dependency-based solution and positioning it in a Shared project (because it will be accessed by all application layers with dependency ):


  
  
  1. public static class Resolver {
  2. public static T GetConcreteInstanceOf<T>() {
  3. return ObjectFactory.GetInstance<T>();
  4. }
  5. }

Resolver class (I like to call it this way, because Microsoft and ASP. net mvc 3 introduce the DependencyResolver class together and I will discuss it later) is a simple static class that contains a function. This function accepts the generic parameter T, which indicates that it is used to find the specific implemented interface and returns T, which is the actual implementation of the incoming interface.

Before I jump to how to use the new Resolver class in your code, I would like to explain why I have compiled a self-developed dependency solution program instead of creating an implementation with ASP.. net mvc 3 introduces the IDependencyResolver interface class. Including IDependencyResolver is a great supplement to ASP. net mvc and has made great progress in promoting correct software behavior. Unfortunately, it resides in System. web. mvc dll, and I do not want to reference libraries specific to Web technologies in non-Web layers of the application architecture.

Parse dependencies in the codeAfter completing all the difficult work, it is easy to parse the dependencies in the code. All you need to do is to call the static GetConcreteInstanceOf function of the Resolver class and pass it to you to find the specific implemented interface for it, as shown inFigure 7.

Figure 7 parse the dependency in the code


  
  
  1. public class EmployeeService : IEmployeeService {
  2. private ILoggingService _loggingService;
  3. public EmployeeService() {
  4. _loggingService =
  5. Resolver.GetConcreteInstanceOf<ILoggingService>();
  6. }
  7. public decimal CalculateSalary(long employeeId) {
  8. _loggingService.LogDebug(string.Format(
  9. "Calculating Salary For Employee: {0}", employeeId));
  10. decimal output = 0;
  11. /*
  12. * Complex logic that needs to be performed
  13. * in order to determine the employee's salary
  14. */
  15. return output;
  16. }
  17. }

Use StructureMap to inject Test Double into unit TestThe code structure has been completed. Therefore, you can inject dependencies without user intervention. Let's go back to the initial task of correctly handling dependencies in unit testing. The specific situation is as follows:

  • This task is written using TDD logic to generate the salary value to be returned from the CalculateSalary method of EmployeeService. (You willFigure 7EmployeeService and CalculateSalary functions are found in .)
  • There is a requirement that all calls to the CalculateSalary function must be recorded.
  • The interface for log service will be defined, but the implementation is incomplete. Currently, an exception is thrown when the logging service is called.
  • The task needs to be completed before the Log service starts as planned.

You may have encountered this type before. But now you have the correct architecture and can get rid of the dependency constraints by implementing Test Double. I like to create a Test Double shared among all my Test projects in a project. For exampleFigure 8As shown in, I have created a Shared project in the Tests solution folder. In this project, I added a Fakes folder, because in order to complete my test, I need the virtual Implementation of ILoggingService.


Figure 8 shared test code and virtual Projects

It is easy to create virtual settings for log service. First, I created a class named LoggingServiceFake In the Fakes folder. LoggingServiceFake must meet the expectations of EmployeeService, which means it must implement ILoggingService and its methods. As defined, virtual hosting is a substitute, containing code that satisfies the interface. In general, this means that it has an empty Implementation of the void method, and the function implementation contains the return statement that returns the hard-coded value, as shown below:


  
  
  1. public class LoggingServiceFake : ILoggingService {
  2. public void LogError(string message, Exception ex) {}
  3. public void LogDebug(string message) {}
  4. public bool IsOnline() {
  5. return true;
  6. }
  7. }

Now virtual settings have been implemented. I can write and test them. At the beginning, I will create a Test class in the TestDrivingMVC. Service. Test. Unit Test project. According to the naming conventions described above, I name it EmployeeServiceTest, as shown inFigure 9.

Figure 9 EmployeeServiceTest test class


  
  
  1. [TestClass]
  2. public class EmployeeServiceTest {
  3. private ILoggingService _loggingServiceFake;
  4. private IEmployeeService _employeeService;
  5. [TestInitialize]
  6. public void TestSetup() {
  7. _loggingServiceFake = new LoggingServiceFake();
  8. ObjectFactory.Initialize(x =>
  9. x.For<ILoggingService>().Use(_loggingServiceFake));
  10. _employeeService = new EmployeeService();
  11. }
  12. [TestMethod]
  13. public void CalculateSalary_ShouldReturn_Decimal() {
  14. // Arrange
  15. long employeeId = 12345;
  16. // Act
  17. var result =
  18. _employeeService.CalculateSalary(employeeId);
  19. // Assert
  20. result.ShouldBeType<decimal>();
  21. }
  22. }

In most cases, the test code is very simple. Note the following code lines:


  
  
  1. ObjectFactory.Initialize(x =>
  2. x.For<ILoggingService>().Use(
  3. _loggingService));

This is the code that instructs StructureMap to use LoggingServiceFake when the Resolver class we created previously attempts to parse ILoggingService. I place this code in a method marked with TestInitialize, which indicates that the unit test framework runs this method before each test in the test class.

By using powerful DI/IoC and StructureMap tools, I can completely get rid of the constraints of the logging service. This enables me to complete coding and unit testing without being affected by the Log service status, and write real unit test code that does not depend on any dependencies.

Use StructureMap as the default controller FactoryASP. net mvc provides an extension that allows you to add custom implementations that instantiate controllers in your applications. By creating a class inherited from DefaultControllerFactory (seeFigure 10), You can control how to create a controller.

Figure 10 custom controller Factory


  
  
  1. public class ControllerFactory : DefaultControllerFactory {
  2. private const string ControllerNotFound =
  3. "The controller for path '{0}' could not be found or it does not implement IController.";
  4. private const string NotAController = "Type requested is not a controller: {0}";
  5. private const string UnableToResolveController =
  6. "Unable to resolve controller: {0}";
  7. public ControllerFactory() {
  8. Container = ObjectFactory.Container;
  9. }
  10. public IContainer Container { get; set; }
  11. protected override IController GetControllerInstance(
  12. RequestContext context, Type controllerType) {
  13. IController controller;
  14. if (controllerType == null)
  15. throw new HttpException(404, String.Format(ControllerNotFound,
  16. context.HttpContext.Request.Path));
  17. if (!typeof (IController).IsAssignableFrom(controllerType))
  18. throw new ArgumentException(string.Format(NotAController,
  19. controllerType.Name), "controllerType");
  20. try {
  21. controller = Container.GetInstance(controllerType)
  22. as IController;
  23. }
  24. catch (Exception ex) {
  25. throw new InvalidOperationException(
  26. String.Format(UnableToResolveController,
  27. controllerType.Name), ex);
  28. }
  29. return controller;
  30. }
  31. }

In this new controller factory, I have a public StructureMap container attribute, which obtains the Set Based on StructureMap ObjectFactory (inFigure 10In Global. asax ).

Next, I have an alternative to the GetControllerInstance method that executes some type of check, and then use the StructureMap container to parse the current controller based on the provided controller type parameters. Because I used the StructureMap Automatic Registration and scanning feature when I first configured StructureMap, no other operations are required.

The benefit of creating a custom controller factory is that your controller is no longer limited to non-parametric constructors. In this case, you may have the following question: "How do I provide parameters to the Controller constructor ?". With the scalability of DefaultControllerFactory and StructureMap, you do not have to provide parameters. When you declare a parameterized constructor for the controller, the dependency is automatically parsed when the controller is resolved in the new controller factory.

For exampleFigure 11As shown in, I have added an IEmployeeService parameter to the HomeController constructor. When the controller is resolved in the new controller factory, all parameters required by the Controller's constructor are automatically parsed. This means that you do not need to manually add code to parse the Controller dependency-but you can still use the virtual settings as described above.

Figure 11 Resolution Controller


  
  
  1. public class HomeController : Controller {
  2. private readonly IEmployeeService _employeeService;
  3. public HomeController(IEmployeeService employeeService) {
  4. _employeeService = employeeService;
  5. }
  6. public ActionResult Index() {
  7. return View();
  8. }
  9. public ActionResult DisplaySalary(long id) {
  10. decimal salary = _employeeService.CalculateSalary(id);
  11. return View(salary);
  12. }
  13. }

By using these practices and technologies in your ASP. net mvc application, the entire TDD process will be easier and more concise.

Keith Burnell Is a senior software engineer at Skyline Technologies. He has been engaged in software development for more than 10 years and specializes in large-scale ASP. NET and ASP. net mvc website development. Burnell is actively involved in the developer community. You can visit dotnetdevdude.com to view his blog, or visit twitter.com/keburnell to view his microblog.

Original article:Http://msdn.microsoft.com/zh-cn/magazine/jj190803.aspx

We sincerely thank the following technical experts for reviewing this article: John Ptacek and Clark NLP

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.