How to write quality (unit) tests?
“Tests are considered a kind of a live documentation for the production code because tests are always kept in sync with the code as opposed to typical text-based documentation” — Test-Driven Development
The first time when I heard this, I loved it, but I didn't understand what it meant. After a while, I ended up on a company that pushed pretty heavy for code coverage (which is my mind is useless since mutation coverage appeared) and unit tests. There I first got into discussions about the naming and I understood how important they can be.
The title has “unit” in the parathesis because most of these “rules”, apply to any kind of test.
- Naming convention
- Story: In the company, I told you about, I ended up in an interesting project, in which as the naming convention for our tests we were using When_Action_Expect_Result. The interesting part was that our testers, created a tool with which they parsed the name and the content of our tests and the generated result was a report with test cases. They ran the tool and they already knew what cases were covered and what they should cover with their e2e and acceptance tests. Another feature of the tool was to get all the names and create some sort of documentation by splitting up based on the underline. You can imagine, that is pretty easy to understand something like: “When SumIsCalledWith5And2 Expect 7”. Here the only con that I found is that you should try your best not to have as action “MethodIsCalled”, because, if the method will be renamed, you’ll need to rename the test also. It would be nice, to have the name “decoupled” from the name of the method. For example, if you have a method named GetExpirationDate(User user), you can try naming something like When_GettingExpirationDateForUserNull_Expect_MySpecificException. Maybe you can find even better naming, but what I wanted to point out is only the convention. For example, Oleksandr Stefanovskyi thinks that the name of the method or class that it’s being tested should be included in the test name (Article can be found here).
- Another interesting conversion is Should_ExpectedBehavior_When_StateUnderTest or even better, to have the Method/ClassName with should and have directly ExpectedBehavior_When_StateUnderTest. I think this one I first saw, on a presentation from Sandro Mancuso (I hope I am not wrong). You can think of something like the test class being named GettingExpirationDateShould and the test being named ThrowMySpecificException_When_UserIsNull
- I also saw Given_Preconditions_When_StateUnderTest_Then_ExpectedBehavior as conversion, but in my mind, you should try something like this ONLY as a start for you, if you want to start using GWT, and you have a lot of knowledge in using the triple-A approach. An example would be Given_ANullUser_When_ExperitionDateIsAsked_Then_MySpecificExceptionIsThrown.
I have read about other conventions, but to be honest, in practice, in production projects, those 3 were the only ones that I saw (and as personal preference is the first one).
Any conversion is good, as long as everyone from the team agrees, use it, and it’s easy to be understood aka, is clear when it is read. DO NOT be afraid of long names! You should know the input and the expected output by reading the test name, without losing time and energy to look in the code of the test (low cognitive complexity). Any human (this means anyone, not only programmers) that read the test name should only have questions to the business to understand what you're trying to test. Do not forget that the names are for people not for computers!
2. Your test should do only one thing, should test one specific requirement. If you have in the name that MySpecificException should be thrown, you shouldn’t verify also if a method was called, or if the result is equal to God know what.
3. For unit tests, you chose what’s a unit. It can be a method, a class, a group of methods, that form a flow, no one can tell you that you’re wrong. My only advice for you is to keep your units as small as possible. I usually prefer, my units to be methods, that do not leave a class. If I call something from another method I try to mock it. This is something that remained in my mind from The Art of Unit Testing written by Roy Osherove
4. In unit testing, you shouldn’t test only the happy paths. You should test everything that can happen to your code. For example, if GetExpirationDate calls a validator to validate the user and that validator can throw SpecificValidationException, well, you should mock the validator to throw this exception and test that the exception is thrown outside, not caught inside your code (I mean you should be sure that your code does not “eat” the exception). I know, it’s stressful, but in my mind, this is good and correct testing and it’s not for coverage!
5. You should test all your logic. If you think it’s not important, why did you write it? Remove it since it might be just noise. For example:
if(!user.IsValid()){throw SpecificValidationException(message:"User is not valid")}
In this case, if you test only if the if statement returns true (1 test), false (another test) and if it throws an exception (3rd test), you’ll get coverage of 100% (doesn’t matter if you use block coverage or line coverage). But what happens with the message? If someone new comes into the project and replace the message by mistake and you have a bug and because of the wrong message you lose hours and hours of debugging, or looking over logs? This is why I said I don’t believe in the code coverage. To fix this and be sure you won’t have this problem you should also have a test in which you check the message from the exception (or maybe include this check on the test in which you check the type of the exception and in that, to expect, the correct exception, type and message). For these cases, to see them easily (in my mind aren’t corner cases), you can use Stryker.NET. It’s easy to integrate and gives you a nice report in the end, with a mutation coverage (which is lower than your test coverage), but should give you higher confidence in your code.
6. On all runs, you should have the same inputs and outputs. You should use random, new guids or anything else. It’s an antipattern to have a random used inside your test.
7. Run your tests in isolation. For unit tests, all the dependencies should be mocked or stubbed and for integration, acceptance and so on, all external calls should be mocked. For any service, or database, or queue that you call from your test, a mock should be actually invoked, that on every call will have the same response. If you don’t want to use mocks, you should create a stub and configure it (and add your pre-conditions) with your own hand. Avoid letting frameworks do your job because you’ll lose control and you’ll get to false negatives sooner than later. For example, when using Moq, you shouldn’t use new Mock<YourInterface>() by default. This will create a mock object with MockBehaviour Default, which actually is MockBehaviour.Loose which means that your mock will never throw exceptions, returning default values, null for reference types, zero for value types and empty enumerable and array. This means that if you forget to make a setup to a method of your mock, it will return the default and it will go forward, without you knowing that you forgot something. You lost control.
//Exampleif(!myService.UserIsSuperman()) {return new Response()};
if(!validator.IsValid(user)){throw SpecificValidationException(message:"User is not valid")}
return new Response()
If the “myService” is a mock with Default/Loose behaviour it will return false and it will return and maybe what I wanted to check is that if the user is valid it will return. This is a false result of the test, only because we don’t have mock behaviour strict. If it was strict, we would have got an exception on the first if saying that we didn't mock UserIsSuperman.
8. The tests should be as fast as possible, and short. You should never see tests more than half of a screen. You should have methods for creating the mocks and the SUT, create the setup for mocks, create the test data, and all should be used as test initializers. If in one test you should have another setup, you should override only that one, not re-create the mocks and the setups each time. It’s noisy! For example in XUnit, the constructor of the test class is called for each test; use this is your advantage!
9. The tests should be independent of the machine on which they are running. “Works on my machine isn’t a thing anymore”. If you think you are a professional, start acting like one!
Do not forget, that will join your project, should only look over your test names and understand what your system does. If he doesn’t understand something, that should only be the ubiquitous language that should be understood with the help of anyone from the team (business people or development team).