DEV Community

Cesar Aguirre
Cesar Aguirre

Posted on

The Art of Unit Testing: Four Takeaways

This is THE book to learn how to write unit tests. It starts from the definition of an unit test to how to implement them at your organization. Although, it covers extensively the subject, it doesn't advocate writing unit tests before or after the production code.

The main takeaway from this book is that you should treat your tests as production code. Sometimes, tests aren't treated as code that needs to be taken care of. You should have test reviews, instead of only code reviews. Your tests are your safety net, so do not let them rot.

Naming convention

Use UnitOfWork_Scenario_ExpectedBehaviour for your test names. You can read it as follow: when calling UnitOfWork with Scenario, then ExpectedBehaviour. A Unit of Work is any logic exposed through public methods that returns value, changes the state of the system or makes an external invocation.

With this naming convention is clear the logic under test, the inputs and the expected result. You will end up with long test names, but it's OK to have long test names for the sake of readability.

Builders vs Setup

Use builders instead of Setup methods. Tests should be isolated from other tests. Sometimes, Setup methods create shared state among your tests. You will find tests that passes in isolation but don't pass alongside other tests and tests that need to be run many times to pass.

Often SetUp methods end up with initialization for only some tests. Tests should create their own world. So, initialize what's need inside every test using builders.

Safe green zone

Keep a set of always passing unit tests. You will need some configurations for your integration tests: a database, environment variables or some files in a folder. Integration tests will fail if those configurations aren't in place. So, developers could ignore some failing tests, and real issues, because of those configurations.

Therefore, separate your unit tests from your integration tests. You will distinguish between a missing setup and an actual problem with your code. A failing test should mean a real problem, not a false positive.

Organization of your tests

Have a unit test project per project and a test class per class. You should easily find tests for your classes and methods. To achieve this:

  • Create separate projects for your unit and integration tests. Add the suffix UnitTests and IntegrationTests accordingly. For example, for a project Library, name your tests projects Library.UnitTests and Library.IntegrationTests.

  • Create tests inside a file with the same name as the tested code adding the suffix Tests. For MyClass, your tests should be inside MyClassTests. Also, you can group features in separate files adding the name of the feature as suffix. For example, MyClassTests.AnAwesomeFeature.

Conclusion

Unit testing is a broad subject. The Art of Unit Testing cover almost all you need to know about it. The main lesson is your tests should be readable, maintainable and trust-worthy. Remember the next person reading your tests will be you.

Top comments (2)

Collapse
 
lamba3 profile image
LaMbA3

Hi, nice post. I have just one question. How to setup configuration for e2e test. For example database, should i create database for tests or so i run tests against already existing database in which case i would also need to deal with data changes. Also how to keep e2e tests independent for example if i test if user can login in some api, i would need to register user before that. What is your opinion?
Thank you for time :)

Collapse
 
canro91 profile image
Cesar Aguirre

Yeah, the problem with e2e tests is they tend to be slow and require a lot of setup. Once in a project I worked in, e2e tests hit the same database and environment that QA team used to manually test and they ended up messing the data/scenarios for the tests. So we opted for manual testing. The general advice is not to have a lot of e2e tests (e.g. testing pyramid)

On the other hand, for integration tests that hit the database, I have tried to use a separated database for the tests and run every test inside a transaction that never commits. You can use a function that wraps your test inside that transaction. So records from a test don't mess with the next test to be run. I applied this idea here. Hope it helps