If tests are hard to write, the production design is crappy - goes an old saying. Indeed, writing unit tests gives you one of the most comprehensive, yet brutal, feedback about the design of your production code, but if it comes too late, many developers can’t stand it anymore and they will either stop testing or test more superficially. At the other extreme, others struggle to write contrived, fragile tests full of mocks that end up frustrating more than helping them. This talk reviews the main hints that unit tests provide you, from the most obvious improvements to some of the most subtle design principles.
The tests are trying to tell you something@VoxxedBucharest.pptx
1. Your tests are trying
to tell you something ...
10 design hints you were missing
Read the article:
https://victorrentea.ro/blog/design-insights-from-unit-testing/
About the speaker:
https://victorrentea.ro
2. victorrentea.ro/training-offer
Hi, I'm Victor Rentea 🇷🇴
Java Champion, 17 years of code, code, code, code, code....
Consultant & Trainer: 5000 developers of 100+ companies in EU:
❤️ Clean Code, Architecture, Unit Testing
🛠 Spring Framework, Hibernate/JPA, Reactive/WebFlux
⚡️ Java Performance, Secure Coding 🔐
Conference Speaker – find many recoded talks online
Founder of Bucharest Software Crafters Community: 5000+ members
🔥 Free monthly Zoom webinars, 1-2 hours after work. Join at victorrentea.ro/community
Past events on my channel: youtube.com/vrentea
Father of 👧👦, woke up at night by a 😺: VictorRentea.ro
3. 3 VictorRentea.ro
From the Agile Ideology ...
Emergent Design
while we keep shipping shit working software fast,
the design of the system will naturally evolve by itself
(as opposed to large up-front design that can cause overengineering)
4. 4 VictorRentea.ro
Writing Tests give you hints
on when to improve the design
Emergent Design
that never emerged
😩
We need triggers!
When should I refine the design?
5. 5 VictorRentea.ro
Kent Beck
Creator of Extreme Programming (XP)
a very technical form of Agile
Inventor of TDD
Author of JUnit
Father of Unit Testing
6. 6 VictorRentea.ro
1. Passes all Tests 💪
2. Expresses Intent = SRP, Domain Names
2. No Duplication = DRY🌵
3. Keep it Simple = KISS💋
Rules of Simple Design
by Kent Beck
https://martinfowler.com/bliki/BeckDesignRules.html
design feedback 💎
10. 10 VictorRentea.ro
Why we 💖 Mocks
Isolated Tests
from external systems 🤔
Fast 🐇
no framework, DB, external APIs
Simpler
when testing high complexity 😵💫
Alternatives:
in-mem DB
Testcontainers 🐳
WireMock
Cheating*
* James Coplien in https://rbcs-us.com/documents/Why-Most-Unit-Testing-is-Waste.pdf
11. 11 VictorRentea.ro
Logic
under test
Cyclomatic Complexity
= number of independent execution paths through code ≈ max no. tests
Test
Test
Test
Test
Test
Test
Test
CC=5 CC=6
f(a) g(b)
calls
f() calling g() together have a CC =
Many
To cover all branches
Heavy
setup and input data
Tests for a+b become...
3
0
15. 15 VictorRentea.ro
The test has 20 lines full of mocks
😵💫
😡
BURN THE TEST!
has a bad cost/benefit ratio
HONEYCOMB TESTING
Outside-in from integration- to unit- test
WHAT AM I TESTING HERE ?
syndrome
Tested prod code has 4 lines
😩
f(..) {
a = api.fetchB(repoA.find(..).getBId());
d = service.createD(a,b,repoC.find(..));
repo3.save(d);
mq.send(d.id);
}
REDESIGN PRODUCTION
Collapse Middle-Man / useless layer
17. 17 VictorRentea.ro
The Bad Monolith ("Big ball of mud") has
- Highly coupled code
- Huge complexity behind a few entry points the need for lots of Unit Tests
Microservices / Modulith
- Huge complexity in a single microservice = bad practice Break µservice/module
- Many APIs, hiding a decent amount of complexity (🙏)
- Easier to test more at the API level Honeycomb Testing Strategy
Testing Microservices is different
18. 18 VictorRentea.ro
Integration
test the entire microservice in isolation
Integrated
may fail because of another system
Implementation Detail
complex parts of the code
Start up all microservices in the ecosystem.
Expensive, flaky, but business-critical e2e tests (eg checkout)
Responsibility of a central QA team(?)
Test end-to-end as many flows in your system.
Starts up the entire system (@SpringBootTest)
Keep tests isolated without mocks
Separate tests for the parts of the code naturally isolated
with high internal complexity.
Mocks are allowed
Still: test roles, not methods/classes
Write social unit tests
Honeycomb Testing Strategy
(suitable for microservices)
Testcontainers 🐳
WireMock
DB ES Kafka ...
API
many tests on
one entrypoint
decouple and test alone
https://engineering.atspotify.com/2018/01/testing-of-microservices/
19. 19 VictorRentea.ro
⚠️
Test manageable complexity without mocks
A B
Instead, test "components" (groups of objects)
#0
Internal refactoring won't break tests
20. 20 VictorRentea.ro
You get most design feedback
from tests in 'Implementation Detail'
Implementation
Detail
22. 22 VictorRentea.ro
var bigObj = new BigObj();
bigObj.setA(a);
bugObj.setB(b);
prod.method(bigObj);
Tests must create bigObj just to pass two inputs🧔
method(bigObj)
MUTABLE
DATA in 2023?
using only 2 of the 15 fields in bigObj
method(a, b)
Precise Signatures prod.method(a, b);
Also, simpler tests:
Pass only necessary data to functions ✅
when(bigObj.getPart1())
.thenReturn(p1);
⛔️ Don't Mock Getters ⛔️
Mock behavior, not data
prod.method(new AB(a, b));
method(ab)
Parameter Object
Testing highly complex logic:
⛔️ Don't have Mocks return Mocks⛔️
23. 23 VictorRentea.ro
🏰
Constrained Objects
= data structures that guard their internal consistency by throwing exceptions,
(eg required fields, string size / regex, or more complex domain rules)
Mutable (eg Domain Entities, Aggregates)
via constructor and setters/mutator methods
Immutable❤️ (Value Objects)
via constructor
24. 24 VictorRentea.ro
A group of classes has a
clear role and manageable complexity (A)
An Object is Constrained ✅ but Large (B)
Object Mother Pattern*
eg. TestData.john(): Customer
coupling
Break Domain Entities
in separate Bounded Contexts
packages > modulith > microservices
Test the entire group with a social unit-test✅:
requires larger setup and larger input
* https://martinfowler.com/bliki/ObjectMother.html (2006)
Same test data factory used in different verticals
customer | order
↓
Break Object Mother per vertical
CustomerTestData | OrderTestData
Creating valid test data gets cumbersome
CREEPY
A shared class creating valid test objects
25. 26 VictorRentea.ro
Your complex logic directly uses
APIs or heavy libraries: Unit-testing your logic
requires understanding the semantics of:
The External API: to populate/assert DTOs
The Library: to mock it
... api.call(apiDetails);
... dto.getStrangeField()
... Lib.use(mysteriousParam,...)
Unit Tests speak your Domain Model
Unit Tests are first-class citizens of your project
#respect them
Agnostic Domain
Isolate complex logic from the outside world
... clientAdapter.call(domainStuff)
... domainObject.getMyField()
... libAdapter.use(😊)
26. application / infra
Value Object
Entity
id
Domain
Service
Domain
Service
agnostic
domain
My DTOs
External
API
External
DTOs
Clien
t
External
Systems
Façade
Controller Repo
IAdapter
Adapter
⛔️
⛔️
Deep complexity is kept inside
for easier testing
Simplified Onion Architecture
Interf
Ugly
Invasive
Library
29. 30 VictorRentea.ro
class Big {
f() { //complex
g();
}
g() { //complex
}
}
Inside the same class,
a complex function f()
calls a complex g()
g() is complex => unit-tested separately
When testing f(), can I avoid executing g()?
That is: can I mock a local method call?
class HighLevel {
LowLevel low;
f() {//complex
low.g();
}
} class LowLevel {
g() {/*complex*/}
}
↓
Partial Mock (@Spy)
Hard to maintain tests:
Which method is real, which is mocked?🧔
Split by Layers of Abstraction
(high-level policy vs low-level details)
Tolerable testing Legacy Code
If splitting the class doesn't feel right,
test f+g together with bigger tests
30. 31 VictorRentea.ro
class HighLevel {
LowLevel low;
f() {//complex
low.g();
}
}
Split by Layers of Abstraction
= vertical split of a class
class LowLevel {
g() {/*complex*/}
}
31. 32 VictorRentea.ro
class Wide {
A a;
B b; //+more dependencies
complexA() {..a.fa()..}
complexB() {..b.fb()..}
}
@ExtendWith(MockitoExtension)
class WideTest {
@Mock A a;
@Mock B b;
@InjectMocks Wide wide;
// 5 tests for complexA()
// 4 tests for complexB()
}
↓
Separate test classes: ComplexATest, ..B..
(keep before useful for all tests 👌)
Complex methods in the same class
use different sets of dependencies:
class ComplexA {
A a; // +more
complexA() {
..a.fa()..
}
}
class ComplexB {
B b; // +more
complexB() {
..b.fb()..
}
}
@BeforeEach
void fixture() {
when(a.fa()).then...
when(b.fb()).then...
}
Split Unrelated Complexity not used when
testing complex1()
what part of the before
is used by my failed test?
** Mockito (since v2.0) throws UnnecessaryStubbingException if a when..then is not used by a @Test, when using MockitoExtension
** This is
BIG
🧔
FIXTURE CREEP
test setup
DRY Principle
32. 33 VictorRentea.ro
Split Unrelated Complexity
= horizontal split a class
class ComplexA {
A a; // +more
complexA() {
..a.fa()..
}
}
class ComplexB {
B b; // +more
complexB() {
..b.fb()..
}
}
36. 37 VictorRentea.ro
BAD HABIT
Mock Roles, not Objects
http://jmock.org/oopsla2004.pdf
You implement a new feature
> ((click in UI/postman)) > It works > Yee-Haa!
OMG, I forgot about unit-tests 😱
...then you write unit tests
by mocking all dependencies
of the prod class you wrote
Several years later, you complain that
your tests are fragile and impede refactoring
Contract-Driven Design
Before mocking a dependency,
clarify its responsibility
=
Changing an API you mocked is painful
😭
37. 38 VictorRentea.ro
"Unit Testing means mocking all dependencies of a class"
- common belief
"It's perfectly fine for unit tests to talk to databases and filesystems!"- Ian Cooper
Unit Testing
= ?
38. 39 VictorRentea.ro
timeframe for developing your feature
When do you start writing tests?
✅ understand the problem => early questions to biz
✅ early design feedback 💎 💎 💎
✅ real test coverage => courage to refactor
41. 42 VictorRentea.ro
1) Has no Side-Effects
(doesn't change anything)
INSERT, POST, send message, field changes, files
2) Same Inputs => Same Output
(no external source of data)
GET, SELECT, current time, random, …
Pure Function
aka "Referential Transparency"
42. 43 VictorRentea.ro
No Network or files
No Changes to Data
No time/random
Pure Functions
use immutable objects❤️
they are super fast
(simplified definition)
43. 44 VictorRentea.ro
a = repo1.findById(..)
b = repo2.findById(..)
c = api.call(..)
🤯complexity🤯
repo3.save(d);
mq.send(d.id);
Very complex logic
using many dependencies
(eg: computePrice, applyDiscounts)
Many tests
using heavy mocking
when(..).thenReturn(a);
when(..).thenReturn(b);
when(..).thenReturn(c);
prod.complexAndCoupled();
verify(..).save(captor);
verify(..).send(...);
x15=😖
Easier to test w/ less mocks ✅
d = prod.pure(a,b,c);
assertThat(d)...
Reduce Coupling of Complex Logic
D pure(a,b,c) {
🤯complexity🤯
return d;
}
Easier to understand ✅
46. 47 VictorRentea.ro
method(Mutable order, d) {
ds.applyDiscounts(order, d);
var price = cs.computePrice(order);
return price;
}
... but you use mutable objects
Swapping two lines can still cause bugs
that is: 4000 ✅ tests, ❌ bugs in production
You have 4.000 unit tests,
100% test coverage 😲
👏
↓
Paranoid Testing
(verifying method call order)
Immutable Objects
method(Immutable order, d) {
var discountedOrder = ds.applyDiscounts(order, d);
var price = cs.computePrice(discountedOrder);
return price;
}
TEMPORAL
COUPLING
Swapping two lines
❌ does not compile
47. 48 VictorRentea.ro
1. Collapse Middle-Man vs "What am I testing here?" Syndrome
2. Honeycomb Testing Strategy vs Fragile microscopic Unit Tests
3. Precise Signatures
4. Tailored Data Structures vs Creepy Object Mother
5. Keep complexity inside Agnostic Domain not on APIs or Extensive
Libraries
6. Separate Complexity by Layers of Abstraction ↕️ vs @Spy
7. Separate Unrelated Complexity ↔️ vs Fixture Creep (bloated setup)
8. Refine Roles (Mock Roles, not Objects) vs blindly @Mock all dependencies
9. More Complexity => Less Dependencies vs Mock-full tests
10.Promote Immutable Objects vs Temporal Coupling
Design Hints from Tests
50. 51 VictorRentea.ro
Unit Testing Reading Guide
1] Classic TDD⭐️⭐️⭐️ (mock-less) https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530
Mock Roles, not Objects ⭐️⭐️⭐️: http://jmock.org/oopsla2004.pdf
"Is TDD Dead?" https://martinfowler.com/articles/is-tdd-dead/
Why Most Unit Testing is Waste (James Coplien): https://rbcs-us.com/documents/Why-Most-Unit-Testing-is-Waste.pdf
vs Integrated Tests are a Scam(J Brains): https://blog.thecodewhisperer.com/permalink/integrated-tests-are-a-scam
2] London TDD⭐️⭐️⭐️ (mockist) https://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627
3] Patterns⭐️ https://www.amazon.com/Art-Unit-Testing-examples/dp/1617290890
4] https://www.amazon.com/xUnit-Test-Patterns-Refactoring-Code/dp/0131495054/
5] (skip through) https://www.amazon.com/Unit-Testing-Principles-Practices-Patterns
51. 52 VictorRentea.ro
Write more automated tests,
earlier, to have time to listen to
what tests are trying to tell you
Join my community to keep in touch,
FOR free monthly, ONLINE debates :
victorrentea.ro
Notes de l'éditeur
I'm victor rentea, I'm a java champion of Romania, working in our field for 17 years.
8 years ago I realized coding was not enough for me, and I started looking around to help the others.
Today this is my full-time job: training and consultancy for companies throughout Europe.
My favorite topics are ...
but of course, to talk about these topics you have to master the frameworks you use, so I do intense workshops on Spring Framework, ....
More senior groups often call me for performance tuning or secure coding. If you want to know more, you can find there my full training offer
Besides the talks at different conferences that you can find online, I try to to one webinar each month for my community.
A few years ago I started this group on meetup to have where to share my ideas and learn from the others in turn.
- This community has exceeded my wildest dreams, turning into one of the largest communities in the world on Software Craftsmanship.
- So what happens there? One day a month we have a zoom online webinar of 1-2 hours after work, during which we discuss one topic and then debate ideas from the participants – usually we have close to 100 live participants, so it's very engaging. If you want to be part of the fun, DO join us, it's completely free.
- Many past events are available on my youtube channel.
- Outside of work, I have 2 kids and a cat that wakes us up in the middle of the night.