To recap, I've set up a domain model structure that consists of:
- Entities - simple classes representing the fields and relationships of my business objects. They consist of properties and methods that act only on those properties (i.e. they have no external dependencies.
- Repository layer - a number of data access methods that provide CRUD methods for retrieving and data from the database and instantiating the entities, and persisting their changes back.
- Service layer - a layer consisting of methods that sit between the UI and repository layer, passing method calls and objects in between.
- Entity validation - these are C# methods defined on the entity object itself to internally validate its fields. They will consist of checks for required fields, range checking and internal field comparisons.
- Service validation - these are checks made against external dependencies. Examples might be user authorisation or arbitrary business rules such as "customer records can only be saved on a Friday".
- Database validation - basically wrappers around database exceptions, such as errors caused due to deleting records where foreign key constraints are violated, or entry of duplicate key values.
- Entity tests - unit tests on the entity self-validation rules
- Service tests - unit tests on the service layer, using a mocked repository
- Controller tests - unit tests on the MVC controller class, using a mocked service layer
- Integration tests - tests that hit the database, running against a known set of data
Customer Entity Test
This test prepares a customer object with a known validation error (a missing name). The rule violations are queried and asserted to check there is a single correct exception reported.
1 private Customer PrepareCustomer()
2 {
3 return new Customer
4 {
5 Id = 1,
6 Name = "Test Customer"
7 };
8 }
9
10 [Description("Entity Test"), TestMethod]
11 public void Customer_InvalidCustomerWithMissingName_GeneratesException()
12 {
13 //Arrange
14 Customer customer = PrepareCustomer();
15
16 //Act
17 customer.Name = "";
18
19 //Assert
20 Assert.AreEqual(1, customer.GetRuleViolations().Count);
21 Assert.AreEqual("Name is a required field.", customer.GetRuleViolations()[0]);
22 }
Customer Service Test
This unit test acts on a method in the service layer. The idea here is to isolate the function and test it without dependencies - i.e. by mocking the repository layer to the test doesn't actually hit the database. In other words, by mocking the repository, we can say given a defined behaviour in the repository method, does the service method respond appropriately. Here we are checking that the correct exceptions are thrown given and valid and invalid Customer object.
1 private CustomersService service = new CustomersService(MockCustomersRepository());
2
3 static ICustomersRepository MockCustomersRepository()
4 {
5 //Set up test data
6 IList<Customer> customers = new List<Customer>
7 {
8 new Customer {Id = 2, Name = "Drama"},
9 new Customer {Id = 1, Name = "Entertainment"}
10 };
11
12 // Generate an implementor of IProductsRepository at runtime using Moq
13 var mockCustomersRepository = new Moq.Mock<ICustomersRepository>();
14 mockCustomersRepository.Setup(x => x.UpdateCustomer(It.IsAny<Customer>())).Verifiable();
15
16 return mockCustomersRepository.Object;
17 }
18
19 private Customer PrepareCustomer()
20 {
21 return new Customer
22 {
23 Id = 1,
24 Name = "Test Customer"
25 };
26 }
27
28 [Description("Service Test"), TestMethod]
29 public void CustomerService_UpdateValidCustomerWithValidAccount_GeneratesNoExceptions()
30 {
31 // Arrange
32 Customer customer = PrepareCustomer();
33 User user = new User { Id = 1, Role = new Role { Id = 1, Name = "Administrator" } };
34
35 // Act: Request the update method for customer that will pass validation and with valid account
36 service.UpdateCustomer(customer, user);
37
38 // Assert: Check the results
39 }
40
41 [Description("Service Test"), TestMethod]
42 public void CustomerService_UpdateCustomerWithMissingName_GeneratesRuleExceptions()
43 {
44 // Arrange
45 Customer customer = PrepareCustomer();
46 customer.Name = "";
47 User user = new User { Id = 1, Role = new Role { Id = 1, Name = "Administrator" } };
48
49 // Act: Request the update method for customer that will fail validation due to missing name
50 bool ok = false;
51 try
52 {
53 service.UpdateCustomer(customer, user);
54 ok = true;
55 }
56 catch (RuleException ex)
57 {
58 // Assert: Check the results
59 Assert.AreEqual(1, ex.Errors.Count);
60 Assert.AreEqual("Name is a required field.", ex.Errors[0]);
61 }
62 Assert.IsFalse(ok);
63 }
Customer Integration Test
This test involves the database, and confirms that the correct information is written to and retrieved from it.
1 [Description("Integration Test"), TestMethod]
2 public void CustomerIntegration_UpdateValidCustomerWithValidAccount_UpdatesCustomerDetails()
3 {
4 //Arrange
5 Customer customer = service.GetCustomer(1);
6 User user = new User { Id = 1, Role = new Role { Id = 1, Name = "Administrator" } };
7
8 //Act
9 customer.Name = "Test Customer Changed";
10 service.UpdateCustomer(customer, user);
11 customer = service.GetCustomer(1);
12
13 //Assert
14 Assert.AreEqual("Test Customer Changed", customer.Name);
15
16 //Revert
17 customer.Name = "Test Customer";
18 service.UpdateCustomer(customer, user);
19 customer = service.GetCustomer(1);
20 Assert.AreEqual(1, customer.Id);
21 Assert.AreEqual("Test Customer", customer.Name);
22 }
Customer Controller Test
In this test we are mocking the service layer, and testing the behaviour of the controller. Again we can say that given a mocked behaviour of the service layer, do the controller methods respond with the appropriate ActionResults (redirects or view rendering).
1 private ICustomersService CustomersService = MockCustomersService();
2
3 static ICustomersService MockCustomersService()
4 {
5 //Set up test data
6 IList<customer> customers = new List<customer>
7 {
8 new customer {Id = 1, Name = "Test Customer"},
9 new customer {Id = 2, Name = "Test Customer Two"}
10 };
11
12 // Generate an implementor of ICustomersService at runtime using Moq
13 var mockCustomersService = new Moq.Mock<ICustomersService>();
14 mockCustomersService.Setup(x => x.GetCustomer(1).Returns(Customers[0]);
15 mockCustomersService.Setup(x => x.UpdateCustomer(It.Is<Customer>(t => t.Name == "Invalid"), null)).Throws(new RuleException(new NameValueCollection { { "Name", "Invalid" } }));
16 return mockCustomersService.Object;
17 }
18
19 [Description("Controller Test"), TestMethod]
20 public void CustomersController_EditValidCustomer_ReturnsCorrectRedirectAction()
21 {
22 // Arrange: get controller
23 CustomersController controller = new CustomersController(CustomersService, usersService);
24 ContextMocks mocks = new ContextMocks(controller);
25
26 // Act: Request the edit action for Customer that will pass validation
27 var result = controller.Edit(new Customer { Id = 3, Name = "Valid" }, null, null);
28
29 // Assert: Check the results
30 Assert.IsNotNull(result);
31 Assert.IsInstanceOfType(result, typeof(RedirectToRouteResult));
32 Assert.AreEqual("Index", ((RedirectToRouteResult)result).RouteValues["action"]);
33 }
34
35 [Description("Controller Test"), TestMethod]
36 public void CustomersController_EditInvalidCustomer_ReturnsViewWithErrorsInModelState()
37 {
38 // Arrange: get controller
39 CustomersController controller = new CustomersController(CustomersService, usersService);
40 ContextMocks mocks = new ContextMocks(controller);
41
42 // Act: Request the edit action for Customer that will fail validation
43 ActionResult result = null;
44 Customer Customer = CustomersService.GetCustomer(3, CustomerLoad.LoadCustomerOnly);
45 Customer.Name = "Invalid";
46 result = controller.Edit(Customer, null, null);
47
48 // Assert: Check the results
49 Assert.IsNotNull(result);
50 Assert.IsInstanceOfType(result, typeof(ViewResult));
51 Assert.AreEqual("Edit", ((ViewResult)result).ViewName);
52 Assert.IsFalse(controller.ModelState.IsValid);
53 Assert.AreEqual(1, controller.ModelState.Count);
54 }
Conclusion
As you can see, this is a very simple, yet fairly comprehensive set of tests on the domain model. Although there's a fair bit of work involved for each set of classes, the patterns are straightforward and hence should be relatively easy for a developer or team to adhere to. Following it I hope is going to lead to a robust domain model, with a consistent and reliable means of validating business rules.
One last note on the integration tests. As they involve hitting the database, they aren't classed as unit tests - they suffer from external dependencies that may lead to brittleness, and they will be a little slower to run due to the need to connect for real to the database. They are therefore a bit more painful to maintain - they can quickly become out of synch with the data, leading to false negative results, and untrustworthy tests.
It's going to take a bit of discipline to maintain this I'm sure - but the way I'm looking to do this is that once I had the bulk of my database structure built, I used the database publishing wizard to generate a script of the full schema and data. This I amended slightly to add statements to drop and create the database, and then checked the file into the solution.
From now on, following any updates to the database schema, I must also make the change to the script. Doing this means that when I come to run the integration tests that are coded to expect particular data values in return, I just need to run the script before running the tests.
Comments
Post a Comment