Some time ago, when refactoring code or adding business logic, my tests failed -> leaving me unsure if I did break something or not.
How to write tests, where you can completely change the implementation and verify that it still works without breaking any test? Feels like a utopia? Come and see how to do this with "real" project example!
3. Little about me
● Coding microservices for ~4 years
● Different languages and technologies:
○ now: Go, python
○ previously: Kotlin, Java
○ MongoDB, SQL, ElasticSearch, Cassandra, Kafka, RabbitMQ, blabla
5. My previous problems with tests
● changing business logic made me change tests a lot
● refactoring made me change tests
● applied “real” TDD only to the simplest code, with structure predicted upfront
Is it worth trying then?
7. But in what context?
When my advices are applicable for me?
● everywhere where you want you software to last and be developed
● when building microservices, as this is what I do right now
● but would try love to try those practices in monolith written from start!
8. Plan for presentation
1. About architecture
2. Unit and Facade
3. Unit and Integration tests
4. Unit tests in BDD style done right
5. Project immutable to refactor - example
6. DO’s and DONT’s
7. TDD goes live!
9. Example project - movie renting site
Simplified requirements:
● view available movies
● rent movie by user
● return movie by user
● prolong rented movie
● get movie recommendations
● ...
(Normally should be some user stories or whatever)
10. Example project - movie renting site
Example user stories:
● As a user I cannot rent more than 2 movies at same time
● As a user I cannot rent movies for which I’m too young
● As a user I cannot rent more movies if I have a fee to pay for keeping to long
● As a user ...
12. What is architecture?
“Architecture is a set of rectangles and arrows, where:
● arrows are directed and
● there are no cycles
Rectangles are on it’s own build from rectangles and
arrows.“
14. What is a unit?
Units:
● units ~~ top-level rectangles
● communicate with other units
○ synchronously or asynchronously
● fulfills specific business cases (single responsibility)
○ made public to the world via a well designed API (called Facade later)
● can have completely different architecture (CQRS, actors, event sourcing...)
23. Tests rules
Unit tests:
● test ONLY by using units public methods (available through Facade)
● do not touch DB or HTTP, etc... check business logic only
● test as much logic as you can using unit tests (test pyramid)
24. Tests rules
Integration tests:
● setup DB to find out if you’re properly ‘integrated’ with it
● example: add movie with HTTP POST , save it into postgres, and verify that
everything can be retrieved properly (HTTP GET)
● never hit any live service (test server, etc)
25. Tests - stolen from BDD
How a test look like?
● use BDD like structure
● use given/when/then to distinguish test parts
● example!
26. BDD user story example
Scenario: As a user I cannot rent more than maximum number of movies at
the same time
Given a user previously rented one movie
And maximum rented movies count is 1
When user wants to rent second movie
Then user cannot rent second movie
27. Tests - Given
Section given:
● used to setup test data and state
● use only test-specific data (reuse global data for common tests)
● use Facade API for setup
○ don’t use repository directly - you can get into invalid logic due to logic change and test can still
pass
● stay minimal -> don’t create 20 objects to test pagination, etc
28. Tests - When
Section when:
● action that is being tested at state set up earlier
● single call of Facade API
29. Tests - Then
Section then:
● verify that things are working correctly
● use Facade to verify expectations
● test should have single reason to fail
35. My DONT’s
● Don’t split logic into too many microservices too early
○ design so that you can extract it easily later
● Don’t let you tests run too slow
○ people will resist to write new (or just stop using TDD)
○ people will run them too rarely -> keep a quick feedback loop
● Don’t keep a test, that does not make you feel safe
○ just delete it
● Don’t create a test for each new class or method
36. My DONT’s
● Don’t mix layers of your application in tests
○ facade is used in each operation in given/when/then
● Don’t be afraid of same path covered in integration and unit test
○ they have different purpose and some are run more frequently
● Don’t overuse table tests
○ seen tests with 2-3 if’s inside based on which data is nil or not
○ better split to distinct tests
37. My DO’s
● test framework/technology is slow or hard to setup? -> change it!
○ example - Kafka Streams -> you can test everything without Kafka running
● use BDD-like given/when/then
○ make your IDE to generate that for you when creating new test
○ use multiple when/then with comment
● make test setup minimal
○ reuse variables
○ extract common things into methods
○ use builders
● after red/green/refactor - take a look at the name and place of a test
38. Biggest DO DO DO!
“ Test behaviour, not implementation”
39. Did it solve my problems?
● proper architecture
● hide logic behind a facade
● encapsulation kept in tests
● keeping given/when/then done right
== Tests immutable to refactor
40. When TDD is best?
When you have NO IDEA how you will code something!!!!!!!!!!!!!!!!
44. When to test single class/method?
● edge cases of simple logic
○ use table driven tests -> https://github.com/golang/go/wiki/TableDrivenTests
● For example some calculations based on numbers -> test all corner cases
45. When to use integration tests?
● heavily relying on DB queries
○ cannot unit test that
○ even then test using facade of your module
● test minimal integration cases, for example to check:
○ if you properly handle events?
○ are HTTP endpoint setup correctly?
○ are DTO’s properly serialized?
46. When to use mocks/stubs?
● communication with other units
● external clients (best if they provide them for you)