KEMBAR78
Unit testing - 9 design hints | PDF
Your tests are trying
to tell you something
by Victor Rentea at
9 design hints you were missing
Read the article:
https://victorrentea.ro/blog/design-insights-from-unit-testing/
Get the code:
https://github.com/victorrentea/unit-testing.git Branch: 9hints_talk
victorrentea.ro
Hi, I'm Victor Rentea
Java Champion – drinking since 2006
Trainer – 3000+ devs in 80+ companies
Speaker – Conferences & Meetups
Hibernate
Spring Advanced FP
Java Performance Secure Coding
Reactive
Architecture Clean Code Unit Testing
pro
Hibernate
Spring Advanced FP
Architecture Clean Code Unit Testing
Masterclass
Company
Training
Video
Lessons
@victorrentea
VictorRentea.ro
victorrentea@gmail.com
Java Performance Secure Coding
Reactive
victorrentea.ro/community
Join My
Community
YouTube
Channel
youtube.com/user/vrentea
335 © VictorRentea.ro
a training by
&
(referring to an old Romanian saying)
- NO COMMENT -
336 © VictorRentea.ro
a training by
▪Better Signatures ⌨= code
▪Immutable Objects ⌨
▪CQS Principle ⌨
▪Functional Core / Imperative Shell ⌨
▪Separation by Layers of Abstraction ⌨
▪Decouple Unrelated Complexity ⌨
▪Role-based Design
▪Onion Architecture
▪Breakdown Data Objects ⌨
Design Improvements hinted by Unit Tests
337 © VictorRentea.ro
a training by
Are you agile?
"Emergent Architecture"
Ever heard of the
338 © VictorRentea.ro
a training by
Are you agile?
Tests give you hints
on how to break
and design complex logic
"Emergent Architecture"
that never emerges 
Under-design is the default today?
339 © VictorRentea.ro
a training by
Do you write Tests
BEFORE the implementation?
Writing #responsible Unit Tests will 🚀 YOUR design skills
If you would, you'd get
✔ more real coverage => courage
✔ more early design feedback
The point of this talk
→ Mocks
340 © VictorRentea.ro
a training by
Mocks make our tests...
Repeatable
isolated from external systems
Fast ✔
DB, external APIs
Reasonably Complex ✔
When facing high cyclomatic complexity ➔
Alternatives:
in-memory DB (H2)
Testcontainers
WireMock
341 © VictorRentea.ro
a training by
Logic
to test
Cyclomatic Complexity
= number of independent paths through code
Test
Test
Test
Test
Test
Test
Test
CC=5 CC=6
f(a) g(b)
calls
CC=30
Too Many
To cover all branches
Too Heavy
in setup and input data (a and b)
End-to-end tests grow...
342 © VictorRentea.ro
a training by
⛔
If there's not much complexity,
avoid mocks
A B
Instead,
test clusters of objects
Internal refactoring
won't break tests 👍
343 © VictorRentea.ro
a training by
(The rest of the talk assumes we have serious complexity)
Unit Testing intensifies design
around complex logic
344 © VictorRentea.ro
a training by
345 © VictorRentea.ro
a training by
Precise Signatures
Methods taking less data as arguments are easier to understand; ✔
and less coupled
method(heavyObject)
HeavyObject having dozens of fields
method(part1, part2)
only takes necessary parts of HeavyObject
Tests will get simpler
new HeavyObject(..)
346 © VictorRentea.ro
a training by
Immutable Objects ✔
eg. Mutating a parameter often causes awkward bugs.
method(obj) {
obj.setX(..);
}
method(..) {
return x;
}
when(b.method(..)).thenReturn("stub");
doAnswer(invocation -> .. obj.setX("stub"))
.when(b).method(any());
Mocking such a method is painful:
347 © VictorRentea.ro
a training by
do side-effects
return void sendEmail(Email):void
Command-Query Separation
setActive(true):void
return results
search(criteria):List
computePrice(flight):int
Bertrand Meyer, in 1994:
Pure Functions
348 © VictorRentea.ro
a training by
No side effects
(doesn't change anything)
No INSERTs, POSTs, queues, files, NO Fields changed,…
𝑒𝑔: 𝑀𝑎𝑡ℎ𝑒𝑚𝑎𝑡𝑖𝑐𝑎𝑙 𝐹𝑢𝑛𝑐𝑡𝑖𝑜𝑛𝑠: 𝑓 𝑥, 𝑦 = 𝑥2
+ 𝑦
+
Referential Transparent
(same inputs produce the same output)
No current time, random, GET, SELECT…
Pure Functions
349 © VictorRentea.ro
a training by
Command-Query Separation Principle
A method should be either
a query (pure function, returning data), or
a command (side-effecting, returning void)
If you need to both stub (when...) and verify the same method,
you are breaking CQS:
Exception: external calls should happen ONCE
eg. REST/WSDL calls that are critical, cost money, or take time
when(b.query(..)).thenReturn(X);
prod.call();
verify(b).command(Y);
when(b.god(..)).thenReturn(X);
prod.call();
verify(b).god(Y);
Apply inside core logic
https://martinfowler.com/articles/mocksArentStubs.html
350 © VictorRentea.ro
a training by
Functional Core / Imperative Shell Segregation
The heavier some logic is, the less dependencies it should have.
Keep complex logic in pure functions. ✔
Complex logic requires many tests.
Shrink these tests by limiting mocks.
=> loose coupling
when.thenReturn(..);
when.thenReturn(..);
when.thenReturn(x);
when.thenReturn(y);
prod.complex();
verify(..).f(z);
verify(..).g();
verify(..).h();
x7=😖
when.thenReturn(..); // ideally 0 mocks
z = prod.complex(x, y);
assert..(z...);
351 © VictorRentea.ro
a training by
Dependencies
Heavy complexity
Side-effects
FUNCTIONAL CORE
PURE
FUNCTION
352 © VictorRentea.ro
a training by
Design the Most Complex Parts of Logic
as Pure Functions 💘
Easier to Understand Easier to Test
353 © VictorRentea.ro
a training by
Separation by Layers of Abstraction
You have two complex functions in the same class, f() calling g()
Move g() in a separate class ✔
You test g() separately.
When testing f(), you don't want it to call g(), as g was tested
Using partial mocks (@Spy) leads to horrid tests. Solution:
class Big {
f() {
//complex
g();
}
g() {
//complex
}
}
class HighLevel {
LowLevel low;
f() {
//complex
low.g();
}
}
class LowLevel {
g() {
//complex
}
}
354 © VictorRentea.ro
a training by
▪Better Signatures
▪Immutable Objects
▪CQS Principle
▪Functional Core / Imperative Shell
▪Separation by Layers of Abstraction
▪Decouple Unrelated Complexity
▪Role-based Design
▪Onion Architecture
▪Breakdown Data Objects
Design Improvements hinted by Unit Tests
355 © VictorRentea.ro
a training by
Fixture Creep
2 complex functions in the same class, use different sets of dependencies.
Break unrelated complexity for clarity and loose coupling. ✔
// prod code
class Wide {
A a;
B b;
complex1() {
//uses a
}
complex2() {
//uses b
}
}
class WideTest {
@Mock A a;
@Mock B b;
@InjectMocks Wide wide;
@BeforeEach
init() {//shared fixture
when(a..).then
when(b..).then
}
// 5 tests for complex1
// 4 tests for complex2
}
class Complex1Test {
@Mock A a;
@Mock B b;
}
class Complex2Test {
@Mock B b;
@Mock C c;
}
Testing both in the same Test class leads to a
bloated fixture ("Before").
Hard to trace later what's used by one test.
Break the test class.
Also: Hierarchical Tests.
356 © VictorRentea.ro
a training by
Mock Roles, not Objects
Bad Habit: You finish the implementation, then you write tests.
You [blindly] @Mock all dependencies.
Two years later, your tests are fragile and slow you down.
Before mocking a dependency, clarify its role. ✔
And test earlier.
http://jmock.org/oopsla2004.pdf
357 © VictorRentea.ro
a training by
Problem: You allow external APIs, DTOs, and ugly frameworks
to enter freely in your core logic.
Testing your core logic now requires creating foreign objects,
or mocking an awkward library.
Onion Architecture
Pull core logic in an Agnostic Domain ✔
So most tests talk in terms of your internal Domain Model ✔
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
application
Value Object
Entity
id
Domain
Service
Domain
Service
(agnostic)
domain
DTO
External
API
DTO
UI,
Client
External
Systems
Façade
Controller
IRepo
Clean Pragmatic Architecture
at Devoxx Ukraine 2021
Curious for more?
↓
IAdapter
Adapter
⛔
⛔
359 © VictorRentea.ro
a training by
Creepy Object Mother
You use large data structures in your core logic.
If the structures enforce their internal consistency,
(it's not an anemic model only having getters and setters)
then creating test data instances becomes cumbersome,
(unrelated fields to set)
so you share a global "TestData" class that builds correct instances.
TestData gets bloated and couples your tests together.
Break TestData🪓 in several classes
Break data structures🪓 in separate Bounded Contexts?
ie packages>modular monolith>microservices🤞
https://martinfowler.com/bliki/ObjectMother.html (2006)
360 © VictorRentea.ro
a training by
362 © VictorRentea.ro
a training by
▪More Calls from Tests > Better Signatures, Better Skills
▪Immutable Objects 🤘 (rock)
▪when.then xor verify > Command-Query Separation
▪More Tests ➔ ↓Mocks > Functional Core / Imperative Shell
▪Partial Mocks > Separation by Layers of Abstraction
▪Bloated Fixture > Break Test > Decouple Unrelated Complexity
▪Mock Roles not Objects > Role-based Design
▪Keep most tests on Domain > Onion Architecture
▪Creepy Object Mother > Break Data Objects
> 2 Bounded Contexts?
Recap
363 © VictorRentea.ro
a training by

Unit testing - 9 design hints

  • 1.
    Your tests aretrying to tell you something by Victor Rentea at 9 design hints you were missing Read the article: https://victorrentea.ro/blog/design-insights-from-unit-testing/ Get the code: https://github.com/victorrentea/unit-testing.git Branch: 9hints_talk victorrentea.ro
  • 2.
    Hi, I'm VictorRentea Java Champion – drinking since 2006 Trainer – 3000+ devs in 80+ companies Speaker – Conferences & Meetups Hibernate Spring Advanced FP Java Performance Secure Coding Reactive Architecture Clean Code Unit Testing
  • 3.
    pro Hibernate Spring Advanced FP ArchitectureClean Code Unit Testing Masterclass Company Training Video Lessons @victorrentea VictorRentea.ro victorrentea@gmail.com Java Performance Secure Coding Reactive victorrentea.ro/community Join My Community YouTube Channel youtube.com/user/vrentea
  • 4.
    335 © VictorRentea.ro atraining by & (referring to an old Romanian saying) - NO COMMENT -
  • 5.
    336 © VictorRentea.ro atraining by ▪Better Signatures ⌨= code ▪Immutable Objects ⌨ ▪CQS Principle ⌨ ▪Functional Core / Imperative Shell ⌨ ▪Separation by Layers of Abstraction ⌨ ▪Decouple Unrelated Complexity ⌨ ▪Role-based Design ▪Onion Architecture ▪Breakdown Data Objects ⌨ Design Improvements hinted by Unit Tests
  • 6.
    337 © VictorRentea.ro atraining by Are you agile? "Emergent Architecture" Ever heard of the
  • 7.
    338 © VictorRentea.ro atraining by Are you agile? Tests give you hints on how to break and design complex logic "Emergent Architecture" that never emerges  Under-design is the default today?
  • 8.
    339 © VictorRentea.ro atraining by Do you write Tests BEFORE the implementation? Writing #responsible Unit Tests will 🚀 YOUR design skills If you would, you'd get ✔ more real coverage => courage ✔ more early design feedback The point of this talk → Mocks
  • 9.
    340 © VictorRentea.ro atraining by Mocks make our tests... Repeatable isolated from external systems Fast ✔ DB, external APIs Reasonably Complex ✔ When facing high cyclomatic complexity ➔ Alternatives: in-memory DB (H2) Testcontainers WireMock
  • 10.
    341 © VictorRentea.ro atraining by Logic to test Cyclomatic Complexity = number of independent paths through code Test Test Test Test Test Test Test CC=5 CC=6 f(a) g(b) calls CC=30 Too Many To cover all branches Too Heavy in setup and input data (a and b) End-to-end tests grow...
  • 11.
    342 © VictorRentea.ro atraining by ⛔ If there's not much complexity, avoid mocks A B Instead, test clusters of objects Internal refactoring won't break tests 👍
  • 12.
    343 © VictorRentea.ro atraining by (The rest of the talk assumes we have serious complexity) Unit Testing intensifies design around complex logic
  • 13.
  • 14.
    345 © VictorRentea.ro atraining by Precise Signatures Methods taking less data as arguments are easier to understand; ✔ and less coupled method(heavyObject) HeavyObject having dozens of fields method(part1, part2) only takes necessary parts of HeavyObject Tests will get simpler new HeavyObject(..)
  • 15.
    346 © VictorRentea.ro atraining by Immutable Objects ✔ eg. Mutating a parameter often causes awkward bugs. method(obj) { obj.setX(..); } method(..) { return x; } when(b.method(..)).thenReturn("stub"); doAnswer(invocation -> .. obj.setX("stub")) .when(b).method(any()); Mocking such a method is painful:
  • 16.
    347 © VictorRentea.ro atraining by do side-effects return void sendEmail(Email):void Command-Query Separation setActive(true):void return results search(criteria):List computePrice(flight):int Bertrand Meyer, in 1994: Pure Functions
  • 17.
    348 © VictorRentea.ro atraining by No side effects (doesn't change anything) No INSERTs, POSTs, queues, files, NO Fields changed,… 𝑒𝑔: 𝑀𝑎𝑡ℎ𝑒𝑚𝑎𝑡𝑖𝑐𝑎𝑙 𝐹𝑢𝑛𝑐𝑡𝑖𝑜𝑛𝑠: 𝑓 𝑥, 𝑦 = 𝑥2 + 𝑦 + Referential Transparent (same inputs produce the same output) No current time, random, GET, SELECT… Pure Functions
  • 18.
    349 © VictorRentea.ro atraining by Command-Query Separation Principle A method should be either a query (pure function, returning data), or a command (side-effecting, returning void) If you need to both stub (when...) and verify the same method, you are breaking CQS: Exception: external calls should happen ONCE eg. REST/WSDL calls that are critical, cost money, or take time when(b.query(..)).thenReturn(X); prod.call(); verify(b).command(Y); when(b.god(..)).thenReturn(X); prod.call(); verify(b).god(Y); Apply inside core logic https://martinfowler.com/articles/mocksArentStubs.html
  • 19.
    350 © VictorRentea.ro atraining by Functional Core / Imperative Shell Segregation The heavier some logic is, the less dependencies it should have. Keep complex logic in pure functions. ✔ Complex logic requires many tests. Shrink these tests by limiting mocks. => loose coupling when.thenReturn(..); when.thenReturn(..); when.thenReturn(x); when.thenReturn(y); prod.complex(); verify(..).f(z); verify(..).g(); verify(..).h(); x7=😖 when.thenReturn(..); // ideally 0 mocks z = prod.complex(x, y); assert..(z...);
  • 20.
    351 © VictorRentea.ro atraining by Dependencies Heavy complexity Side-effects FUNCTIONAL CORE PURE FUNCTION
  • 21.
    352 © VictorRentea.ro atraining by Design the Most Complex Parts of Logic as Pure Functions 💘 Easier to Understand Easier to Test
  • 22.
    353 © VictorRentea.ro atraining by Separation by Layers of Abstraction You have two complex functions in the same class, f() calling g() Move g() in a separate class ✔ You test g() separately. When testing f(), you don't want it to call g(), as g was tested Using partial mocks (@Spy) leads to horrid tests. Solution: class Big { f() { //complex g(); } g() { //complex } } class HighLevel { LowLevel low; f() { //complex low.g(); } } class LowLevel { g() { //complex } }
  • 23.
    354 © VictorRentea.ro atraining by ▪Better Signatures ▪Immutable Objects ▪CQS Principle ▪Functional Core / Imperative Shell ▪Separation by Layers of Abstraction ▪Decouple Unrelated Complexity ▪Role-based Design ▪Onion Architecture ▪Breakdown Data Objects Design Improvements hinted by Unit Tests
  • 24.
    355 © VictorRentea.ro atraining by Fixture Creep 2 complex functions in the same class, use different sets of dependencies. Break unrelated complexity for clarity and loose coupling. ✔ // prod code class Wide { A a; B b; complex1() { //uses a } complex2() { //uses b } } class WideTest { @Mock A a; @Mock B b; @InjectMocks Wide wide; @BeforeEach init() {//shared fixture when(a..).then when(b..).then } // 5 tests for complex1 // 4 tests for complex2 } class Complex1Test { @Mock A a; @Mock B b; } class Complex2Test { @Mock B b; @Mock C c; } Testing both in the same Test class leads to a bloated fixture ("Before"). Hard to trace later what's used by one test. Break the test class. Also: Hierarchical Tests.
  • 25.
    356 © VictorRentea.ro atraining by Mock Roles, not Objects Bad Habit: You finish the implementation, then you write tests. You [blindly] @Mock all dependencies. Two years later, your tests are fragile and slow you down. Before mocking a dependency, clarify its role. ✔ And test earlier. http://jmock.org/oopsla2004.pdf
  • 26.
    357 © VictorRentea.ro atraining by Problem: You allow external APIs, DTOs, and ugly frameworks to enter freely in your core logic. Testing your core logic now requires creating foreign objects, or mocking an awkward library. Onion Architecture Pull core logic in an Agnostic Domain ✔ So most tests talk in terms of your internal Domain Model ✔ https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
  • 27.
  • 28.
    359 © VictorRentea.ro atraining by Creepy Object Mother You use large data structures in your core logic. If the structures enforce their internal consistency, (it's not an anemic model only having getters and setters) then creating test data instances becomes cumbersome, (unrelated fields to set) so you share a global "TestData" class that builds correct instances. TestData gets bloated and couples your tests together. Break TestData🪓 in several classes Break data structures🪓 in separate Bounded Contexts? ie packages>modular monolith>microservices🤞 https://martinfowler.com/bliki/ObjectMother.html (2006)
  • 29.
  • 31.
    362 © VictorRentea.ro atraining by ▪More Calls from Tests > Better Signatures, Better Skills ▪Immutable Objects 🤘 (rock) ▪when.then xor verify > Command-Query Separation ▪More Tests ➔ ↓Mocks > Functional Core / Imperative Shell ▪Partial Mocks > Separation by Layers of Abstraction ▪Bloated Fixture > Break Test > Decouple Unrelated Complexity ▪Mock Roles not Objects > Role-based Design ▪Keep most tests on Domain > Onion Architecture ▪Creepy Object Mother > Break Data Objects > 2 Bounded Contexts? Recap
  • 32.