Unit testing best Practices for domain-driven design (ii)
All along, I tried to find an effective unit test pattern, so that "unit testing" really can be popular in the team, so that unit testing is no longer a formality, but to make unit testing a way to improve the quality of the code.
This article describes a scenario for implementing unit tests for domain-driven projects implemented in EF Code first mode.
Before describing this scenario, let's look at what this best practice is all about and what is ultimately achieved:
1, take the MVC project as an example, if the focus of unit testing on how to test a controller or action will have little effect, for two reasons:
- In principle, the controller does not contain business logic, in theory, most of the code is ViewModel and DTOs between the assignment or service invocation, the code to write unit tests have little effect, very low cost.
- The controller's code is highly dependent on the UI, which means that the controller's code is not stable enough, which will force the unit test to change too frequently, and it is easy for developers to create unit tests as a burden.
For this reason, I would not recommend the team to write unit tests on the controller with a tight manual.
2, a software project really need to test the center of gravity is the business logic, for a domain-driven project, the domain logic is the center of gravity. But we know that domain logic is inseparable from data support, which means we need to deal with repository.
For such a test scenario, most tutorials will prompt you to mock Repository, which is undoubtedly correct in terms of unit testing, but there are two problems with such a scenario:
- The actual experience tells us that such tests do not really reflect the problem of the code, even the unit test is passed, but the debug is a problem. The reason for this is that we ignore the database part and this part of the logic is out of control.
- There is too much data to mock, sometimes in order to test a logic, the mock code is more than the test, and to create unit testing for the developer is actually playing the wrong understanding of the mock.
So the ideal unit test in my mind should have the following conditions:
- Testing is done from Service->repository->domain a line, and the test can accurately reflect how the code works. So, to be precise, I think this project should be called "Integration test of domain-driven design".
- Try not to mock, including reading the database section.
- The data required for testing should be reusable, and the business logic for testing "registered user", "Search user" should be able to reuse the data provided by the test.
- Any test can run independently, and the effect of multiple executions of the same test should be consistent and the test executed as fast as possible.
In order to be as close as possible to this goal, I implemented a very simple DDD case for testing, which describes two important domain models: The user domain model describes the logic of "registered user", "Change Password", "login", etc. The bookmanageprocess domain model describes the logic of "borrowing books" and "Returning books", which you can understand as a model of library borrowing and return.
To be able to understand this test scenario, I'll make a brief description of the test case:
This case is based on a DDD case implemented by EF Code First and Castle, which is also tailored for DDD and is not suitable for a traditional three-tier architecture.
As the solution shows, this is a very simple case, I gave him a very domineering name: Mvctests.bestpractice, as for why called Mvctests, because the test scheme can be used in the MVC+DDD architecture, However, due to the very low cost-performance of the controller writing tests, the controller test appears in this scenario.
Why is this case a domain driven case?
Take the "User registration" function as an example, let us analyze:
1, from the UserService this entrance to see:
public class Userservice:applicationservice, Iuserservice {private readonly iuserrepository _userreposit Ory Private ReadOnly Iemailuniquechecker _emailuniquechecker; Public UserService (Irepositorycontext context, Iuserrepository Userrepository,iemailuniquechecker Emailuniquechecker): Base (context) {_userrepository = userrepository; _emailuniquechecker = Emailuniquechecker; The public Guid Register (Usermodel usermodel) {var user = User.register (usermodel,_emailuniquechec Ker); _userrepository.add (user); Context.commit (); return user. Id; }}
The Register () method is almost only a call to the domain Model User.register () method, and the rest of the code is negligible, indicating the fact that the service layer does not have any business logic and that all logic should be in domain.
2. Implementation of the Register () method in the user domain model:
Public partial class User {public static user Register (Usermodel usermodel, Iemailuniquechecker emailuniquechec Ker) {contract.requires (!usermodel.name.isnullorempty (), "Invalid username"); if (Emailuniquechecker.isexist (Usermodel.email)) {throw new Duplicateemailexception ("Email alre Ady exist, please input another one "); } var password=new password (usermodel.password); var user = new User () {Id = Guid.NewGuid (), Name = Usermodel.name, Password = Password. Hashedpassword, Salt = password. Salt, Email = usermodel.email, Registerdatetime = DateTime.Now, lastlogindate Time = DateTime.Now}; return user; }}
First, this is a patial class, because the other part describes the contents of the property being used by EF to manipulate the database. There are two main logic in this approach:
Check the email and encrypt the password, as you can see: These logic reflects what the actual logic of registering a user is, and all of that logic should belong to domain.
Because of the inability to make dependency injection in domain, we passed the Iemailuniquechecker component from the service layer through a method, which is implemented as follows:
public class Emailuniquechecker:iemailuniquechecker { private readonly iuserrepository _userrepository; Public Emailuniquechecker (iuserrepository userrepository) { _userrepository = userrepository; } public bool Isexist (string email) { var user = _userrepository.find (x = x.email.tolower () = = email. ToLower ()). FirstOrDefault (); return user! = null;} }
and the password class to abstract the "password" business rules, the same abstraction should belong to domain, let us take a look at his partial implementation:
public class Password {public byte[] Hashedpassword {get; private set;} Public byte[] Salt {get;} Public Password (String Password) {assertpasswordmatchespolicy (Password); Salt = Guid.NewGuid (). Tobytearray (); Hashedpassword = Hashpassword (Salt:salt, Password:password); private void Assertpasswordmatchespolicy (string password) {if (password = = null) { var error = seq.create ("Password can not is null"); throw new Passworddoesnotmatchpolicyexception (error); } var errors = new list<string> (); if (password. Trim (). Length < 6) {errors. ADD ("password shorter than six characters"); } if (password. ToLower () = = password) {errors. ADD ("Password missing uppercase characters"); } if (password. ToUpper () = = password) {errors. ADD ("Password missing lowercase characters"); } if (errors. Any ()) {throw new passworddoesnotmatchpolicyexception (errors); } }}
If not because of the existence of the password class, all of this code should be written in the register () method of the user domain model.
Continue to analyze the "User logon" process:
1, UserService in the entrance:
public bool Login (string email, string password) { var user = _userrepository.find (x = x.email.tolower () = = Em Ail. ToLower ()). FirstOrDefault (); if (user = = null) { throw new Applicationserviceexception ("No such user"); } if (!user. Login (password)) { return false; } _userrepository.update (user); Context.commit (); return true; }
The first part of the code we can think of by email to get the user domain model, read to the domain model after the call user. Login () method. This also illustrates the fact that the service layer does not have any business logic and that all logic should be in domain.
2. Login implementation in the user domain model:
public bool Login (string password) { contract.requires (!password. IsNullOrEmpty (), "Password can not is empty"); var Hashedpassword = new Password (Password, Salt); if (Hashedpassword.iscorrectpassword (password)) { lastlogindatetime = DateTime.Now; return true; } return false; }
As you can see: These logic reflects what the actual logic of a user's login is, and all of that logic should belong to domain.
The entire program code is available for download: Https://git.oschina.net/richieyangs/MvcTests.BestPractice.git
Unit testing best Practices for domain-driven design (i)