“Quality is free, but only to those who are willing to pay heavily for it.”
– T. DeMarco and T. Lister
Nowadays writing good tests for your application is the crucial part of modern software development. Applications with good test coverage have less bugs and are much easier to change and understand.
Ideally, the well-tested application should have 3 kinds of tests – unit, integration, and E2E tests in proportion described as “TestPyramid” .
On another side, to be effective tests should deliver 3 major qualities:
If code under test is broken, that test should fail.
If code under test is working, test verifying it should pass.
Failed tests should precisely tell what is the problem and where it is located.
You can read more about these qualities in awesome art Testing on the Toilet: Effective Testing.
Let’s take a deeper look how different kinds of test help to deliver these qualities.
End 2 End tests
Here we some to the very top of out pyramid – E2E tests. This kind of application testing verifies if some feature of the application is working or not. but they have very high fidelity so they are very fragile and easy to break.
It might seem to be very appealing to write a lot of E2E tests because they seem to be cheap – after running one test you will know if app works or not. Wow! Actually not so fast!
The trick is that E2E tests are extremely hard to debug and absolutely awful on narrowing to the actual problem. In the best case, you will get something like “Selector was not found, Screw you!”.
Some smart people advice in course testing on the toilet to follow these rules writing good E2E tests:
- Have one E2E test for each major use-case
Rest should be covered by integration and unit tests.
- Use debug logging while executing E2E tests.
It provides additional feedback and helps to narrow down the problem.
- Focus on checking system behavior, but not CSS and implementation details
- Allocate time to fix E2E tests :troll
Checking if specific class works as expected, Unit tests usually operate on the code level. Well-written applications treat unit tests as necessary explanatory part of the code base.
They also help to achieve high precision and narrow the problem in the code. It’s far easier to read the message about unexpected return type in specific function or class than figuring out roots of “UserNotSavedException“.
Tools are more language and framework specific, so it’s not a big deal to choose the right tool for unit testing that fits for you.
Unit tests have a lot of advantages, but they still don’t solve one big problem – making sure that all parts of the application work well together.
It’s a quite common scenario when all the tests are green but the application still doesn’t work because of a mistake in gluing different parts together.
Integration tests might be more time consuming since they involve fake systems to interact, like in-memory databases, message queues, external services.
This kind of testing can be used to validate if some par to the system is working as expected after a change, for example, refactoring of the module, or changing a library.
I hope that you found article helpful and learned something new about writing good tests. If you have something to add, please don’t hecitate to comment, if you liked the post, please subscribe and share the article.