Testing is fundamental in software development. Quality gates demand high coverage levels, pull requests need sufficient tests, leading to teams spending considerable time writing and maintaining them. But are we using our tests to their full potential?
'If code is hard to test, the design can be improved'. Starting from this mantra, this deep-dive session unveils hints to simplify code, break-down complexity, and effectively use functional programming. We'll delve into topics like fixture creep, partial mocks, onion architecture, and pure functions, providing numerous best practices and practical tips for your testing.
Be warned: This session may significantly disrupt your work routine and will likely change how you see testing. Attend at your own risk.
1. Test-Driven Design Insights
10 design hints you were missing
== The Deep Dive ==
Article: https://victorrentea.ro/blog/design-insights-from-unit-testing/
Code: https://github.com/victorrentea/unit-testing.git on branch: devoxx-be-2023
About Victor Rentea: https://victorrentea.ro
2. victorrentea.ro/training-offer
👋 Hi, I'm Victor Rentea 🇷🇴 PhD(CS): VictorRentea.ro
Java Champion, 18 years of code, 10 years of teaching
Consultant & Trainer for 120+ companies:
❤️ Clean Code, Architecture, Unit Testing
🛠 Spring, Hibernate, Reactive/WebFlux
⚡️ Java Performance, Secure Coding 🔐
Lots of Conference Talks on YouTube
Founder of European Software Crafters Community (6K members)
🔥 Free 1-hour webinars, after work 👉 victorrentea.ro/community
Past events on youtube.com/vrentea
Father of 👧👦, servant of a 🐈, weekend gardener 🌼 VictorRentea.ro
3. 3
From the Agile (2001) Ideology ...
Emergent Design
While we keep shipping shit,
the design of the system design will naturally improve
(as opposed to large up-front design that caused overengineering in waterfall)
4. 4
Writing Unit Tests
gives you those triggers!
Emergent Design
that never emerged
We need triggers
to start improving the design!
I f t e s t s a r e h a r d t o w r i t e ,
t h e d e s i g n s u c k s
5. 5
Kent Beck
Creator of Extreme Programming (XP) in 1999
the most technical style of Agile
Inventor of TDD
Author of JUnit
Father of Unit Testing
6. 6
1. Passes all Tests ⭐️
Proves it works as intended
Safe to refactor later
Massive feedback on your design 💎
2. Clear, Expressive, Consistent, Domain names
2. No duplication = DRY🌵
3. Minimalistic = KISS💋, avoid overengineering
Rules of Simple Design
https://martinfowler.com/bliki/BeckDesignRules.html
Keep it Short and Simple!
by Kent Beck
11. 11
Why we 💖 Mocks
Isolated Tests
from external systems
Fast 👑
no framework, DB, external API
Test Less Logic
when testing high complexity 😵💫
Alternatives:
- In-mem DB
- Testcontainers 🐳
- WireMock, ..
12. 12
public computePrices(...) {
// A
for (Product product : products) { +1
// B
if (price == null) { +1
// C
}
for (Coupon coupon : customer.getCoupons()) { +1
if (coupon.autoApply() +1
&& coupon.isApplicableFor(product, price) +1
&& !usedCoupons.contains(coupon)) { +1
// D
}
}
}
return ...;
}
Code Complexity - Cyclomatic
- Cognitive (Sonar)
13. 13
Logic
under test
Testing Complex Code
More execution paths through code => More tests
Test
Test
Test
Test
Test
Test
Test
complexity=5 =6
f(a) g(b)
calls
f() calling g() together have a complexity of up to ...
Too Many
(to cover all branches)
Too Heavy
(setup and input data)
Tests for f() calling g() become...
30
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
15. 15
Why we 💖 Mocks
Isolated Tests
from external systems
Fast 👑
no framework, DB, MQ, external API
Test Less Logic
when testing high complexity 😵💫
Alternatives:
- In-mem DB (H2)
- Testcontainers 🐳
- WireMock, ..
🤨 Cheating
James Coplien in https://rbcs-us.com/documents/Why-Most-Unit-Testing-is-Waste.pdf
16. 16
Why we 🤨 Mocks
Uncaught Bugs in Production 😱
despite 1000s of ✅GREEN tests
(lock, tap, doors, umbrella)
Fragile Tests 💔
that break at any refactoring
Unreadable Tests 😵💫
eg. a test using ≥ 5 mocks
19. 19
Test has 20 lines full of mocks
😵💫
😡
BURN THE TEST!
bad cost/benefit ratio
HONEYCOMB TESTS
Prefer Integration/Social Tests
WHAT AM I TESTING HERE ?
syndrome
Tested prod code has 4 lines
😩
f(..) {
a = api.fetchB(repo1.find(..).getBId());
d = service.createD(a,b,repo2.find(..));
repo3.save(d);
mq.send(d.id);
}
g(dto) {
repo2.save(mapper.fromDto(dto, repo1.find(..)));
}
SIMPLIFY PRODUCTION
"Remove Middle-Man"
(inline method / collapse layer)
21. 21
The Legacy Monolith
can hide terrible complexity behind a few entry points
Cover deep corners of logic with Unit Tests
Microservices
have more APIs (stable test surfaces),
hide less complexity (but more configuration risk)
Test more at API-level (Integration) Honeycomb Testing Strategy
22. 22
Integration
(one microservice)
Integrated
(entire ecosystem)
Deploy all microservices in dockers / staging env
Expensive, slow, flaky tests
Use for business-critical flows (eg checkout)
Cover one microservice fully
Use for: Default for every flow ⭐️⭐️⭐️
Isolate tests without mocks
Cover 1 class/role (Solitary/Social Unit Tests)
Use for: naturally isolated pieces with high complexity.
@Mock are allowed
Honeycomb Testing Strategy
Testcontainers 🐳
WireMock
&co
Contract
Tests
(Pact, SCC)
DB ES Kafka ...
API
many tests on
one entry point
https://engineering.atspotify.com/2018/01/testing-of-microservices/
Implementation Detail
(a role)
complexity
decouple and
test in isolation
23. 23
Unit Tests give you most Design Feedback 💎
More Complexity => Better Design
Implementation
Detail Tests
👍
MOCKS
complexity
You can black-box-test
horrible, terrible,
unmaintainable code
25. 25
The Dawn of Mocks
(2004)
http://jmock.org/oopsla2004.pdf
26. 26
BAD HABIT
Mock Roles, not Objects
http://jmock.org/oopsla2004.pdf
You implement a new feature
> ((click in UI/postman)) > It works > 🎉
Oh NO!! I forgot to write tests 😱
...then you write unit tests
blindly mocking all the dependencies
of the prod code you wrote
↓
Few years later:
"My tests are fragile and impede refactoring!"
Contract-Driven Design
Before mocking a dependency,
clarify its responsibility
=
After you mock an API, it freezes ❄️
(changing it = pain)
😭
27. 27
strive to write
Social Unit Tests
for components (groups of objects) with a clear responsibility
✅ Internal refactoring of the component won't break the tests
❌ More complexity to understand! More mocks to face?
Instead of fine-grained Solitary Unit Tests testing one class in isolation,
A B
<
>
29. 29
"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 in TDD, Where Did It All Go Wrong
Unit Testing
= ?
Robust Unit Testing requires
identifying responsibilities
#0
31. 31
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.getA()).thenReturn(a);
⛔️ Don't Mock Getters ⛔️
prod.horror(new ABC(a, b, c));
horror(abc)
Parameter Object
class ABC {a,b,c}
When testing highly complex logic, introduce a
⛔️ Don't return Mocks from Mocks ⛔️
(mockA)
✅ Mock behavior. ✅ Construct test data.
32. 32
🏰
Constrained Objects
= data structures that throw exceptions on invalid data
Customer{email ≠ null}, Interval{start<end}
Mutable: Domain Entities, Aggregates
Immutable❤️: Value Objects
33. 33
❶ A Constrained Object gets Large
↓
Object Mother Pattern
TestData.customer(): a valid Customer
coupling
Break Domain Entities
InvoicingCustomer | ShippingCustomer
in separate Bounded Contexts
packages > modules > microservices
❷ Integration and Social Unit Tests❤️
require heavy data inputs
* https://martinfowler.com/bliki/ObjectMother.html (2006)
Same Object Mother used in different verticals
invoicing | shipping
↓
Split Object Mother per vertical
InvoicingTestData | ShippingTestData
Creating valid test data gets cumbersome
CREEPY
A large class shared by many tests
Don't change it: only add code (lines/methods).
Each tests can tweak the objects for their case.
✅ TestData.musk(): Customer (a persona)
🏰
35. 35
Tests require detailed understanding of:
External API/DTOs
The Library: to mock it
dto = externalApi.call(apiDetails);
... dto.getFooField()
... Lib.use(mysteriousParam,...)
Tests should speak your Domain Language
#respect4tests
Agnostic Domain
Isolate complex logic from external world
via Adapters
obj = clientAdapter.call(domainStuff)
... myDomainObject.getMyField()
... libAdapter.use(😊)
Your complex logic directly uses
External API/DTOss or heavy libraries:
36. 36
application / infra
My DTOs
External
API
External
DTOs
Clien
t
External
System
Application
Service
Simplified Onion Architecture
(more in my "Clean Pragmatic Architecture"
at Devoxx Ukraine 2021 on YouTube)
Value Object
Entity
id
Domain
Service
Domain
Service
agnostic
domain
Repo
IAdapter
Adapter
⛔️
⛔️
IWrapper
Ugly Library
Domain Complexity Protected Inside
37. 37
#1 Unit Testing encourages
Minimal Signatures
Tailored Data Structures
Agnostic Domain
39. 39
Any problem in computer science
can be solved by introducing
another level of abstraction.
- David Wheeler (corrupted quote)
40. 40
class BigService {
f() { //complex
g();
}
g() { //complex
}
}
Inside the same class,
a complex function f()
calls a complex g()
g() grew complex => unit-test it alone?
When testing f(), can I avoid entering g()?
Can I mock a local method call?
class HighLevel {
LowLevel low;
f() {//complex
low.g();
}
} class LowLevel {
g() {/*complex*/}
}
↓
Partial Mock (@Spy)
Hard to understand tests:
Which prod method is real, which is mocked?
🧔
Split by Layers of Abstraction
(high-level policy vs low-level details)
Only use it when
testing Legacy Code
If splitting the class breaks cohesion,
test f() + g() together with social unit tests
⛔️
41. 41
class HighLevel {
LowLevel low;
f() {//complex
low.g();
}
}
Split by Layers of Abstraction
= vertical split of a class
class LowLevel {
g() {/*complex*/}
}
horizontal
placeOrder(o)
processPayment(o)
42. 42
class Wide {
A a;
B b; //+more dependencies
f() {..a.a()..}
g() {..b.b()..}
}
@ExtendWith(MockitoExtension)
class WideTest {
@Mock A a;
@Mock B b;
@InjectMocks Wide wide;
// 5 tests for f()
// 7 tests for g()
}
↓
Split the test class in WideFTest, WideGTest
(rule: the fixture should be used by all tests)
Unrelated complex methods in the same class
use different sets of dependencies:
class ComplexF {
A a;
f() {
..a.a()..
}
}
class ComplexG {
B b;
g() {
..b.b()..
}
}
@BeforeEach
void sharedFixture() {
when(a.a()).then...
when(b.b()).then...
}
Split Unrelated Complexity
Part of the setup
is NOT used by a test.
** Mockito (since v2.0) throws UnnecessaryStubbingException if a when..then is not used by a @Test, when using MockitoExtension
Unmaintainable Tests
🧔
FIXTURE CREEP
= test setup
DRY
https://www.davidvlijmincx.com/posts/setting_the_strictness_for_mockito_mocks/
use
43. 43
Split Unrelated Complexity
class ComplexF {
A a;
f() {
..a.a()..
}
}
class ComplexG {
B b;
g() {
..b.b()..
}
}
horizontally ↔️
getProduct() createProduct()
44. 44
Vertical Slice Architecture (VSA)
https://jimmybogard.com/vertical-slice-architecture/
class GetProductUseCase {
@GetMapping("product/{id}")
GetResponse get(id) {
...
}
}
horizontally ↔️
Organize code by use-cases, not by layers
class CreateProductUseCase {
@PostMapping("product/{id}")
void create(CreateRequest) {
...
}
}
46. 46
Should I mock it? – Practical Examples in a Spring app
Repository (eg Spring Data JpaRepository)?
✅YES: clear responsibility with stable API + expensive to integration test
Message Sender (eg RabbitTemplate)?
✅YES: clear responsibility, simple API + expensive to integration-test
Flexible library class (RestTemplate/WebClient) ?
❌NO: complex API Integration Test with WireMock?
Adapter wrapping an external API call ?
✅YES: allow to keep your tests agnostic
Satellite class (simple DtoEntity mapper) ?
❌NO: too simple Social / Integration Test
A logic component (@Service) ?
✅YES: if clear, non-trivial responsibility: NotificationService, UpdateProductStockService
❌NO if unclear role: ProductService doing "anything" Social/Integration Test
A Data Structure (@Entity/@Document/Dto/...) ?
❌ don't mock populate an instance
47. 47
#3 Unit Testing promotes
Functional Programming values
Pure Functions &
Immutable Objects
48. 48 VictorRentea.ro
a training by
No Side-Effects
(causes no changes)
INSERT, POST, send message, field changes, files
Same Input => Same Output
(no external source of data)
GET, SELECT, time, random, UUID …
Pure Functions
just calculates a value
aka
Referential
Transparent
49. 49
No Network or files
No Changes to Data
No time or random
Pure Functions
(When using a DI container)
50. 50
@VisibleForTesting
a = repo1.findById(..)
b = repo2.findById(..)
c = api.call(..)
🤯complexity🤯
repo3.save(d);
mq.send(d.id);
Complex logic
using many dependencies
(eg: computePrice, applyDiscounts)
Many tests
using lots of mocks
when(..).thenReturn(a);
when(..).thenReturn(b);
when(..).thenReturn(c);
prod.complexAndCoupled();
verify(..).save(captor);
d = captor.get();
assertThat(d)...
verify(..).send(...);
✅ Simpler tests (less mocks)
d = prod.pure(a,b,c);
assertThat(d)...
Reduce Coupling of Complex Logic
Clarify inputs/outputs
D pure(a,b,c) {
🤯complexity🤯
return d;
}
extract
method
55. 55
method(Mutable order, discounts) {
ds.applyDiscounts(order, discounts);
var price = cs.computePrice(order);
return price;
}
... but you use mutable objects
bugs in production ❌
despite 4000 ✅ tests
You have 4.000 unit tests,
100% test coverage 😲
👏
↓
Paranoid Testing
(InOrder can verify method call order)
Immutable Objects
method(Immutable order, d) {
var discountedOrder = ds.applyDiscounts(order, d);
var price = cs.computePrice(discountedOrder);
return price;
}
TEMPORAL
COUPLING
compilation fails ❌
If you swap two lines ...
If you swap two lines ...
56. 56
1. Collapse Middle-Man vs "What am I testing here?" Syndrome or bigger tests:
2. Honeycomb Testing Strategy = more Integration Tests over Fragile Unit Tests
3. Precise Signatures: simpler arguments, better names
4. Dedicated Data Structures vs Creepy Object Mother
5. Agnostic Domain vs mixing core logic with External-APIs or Heavy-Library calls
6. Split Complexity by Layers of Abstraction ↕️ vs Partial Mocks (aka spy)
7. Split Unrelated Complexity ↔️ vs Fixture Creep (large shared setup)
8. Clarify Roles, Social Unit Tests vs blindly mock all dependencies
9. Decouple Complexity in Pure Functions vs Many tests full of mocks
10.Immutable Objects vs Temporal Coupling
Design Hints from Tests
58. 58
timeframe for developing your feature
When do you start writing tests?
✅ understand the problem
✅ early design feedback 💎 💎 💎
✅ confidence to refactor later
Too Late
TDD Early Enough
60. 60
Unit Testing Reading Guide
1] Classic TDD⭐️⭐️⭐️ (mock-less) https://www.amazon.com/Test-Driven-Development-Kent-Beck
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 +
Demo Exercise by Sandro Mancuso https://www.youtube.com/watch?v=XHnuMjah6ps
3] Patterns⭐️ https://www.amazon.com/Art-Unit-Testing-examples
4] https://www.amazon.com/xUnit-Test-Patterns-Refactoring-Code
5] (skip through) https://www.amazon.com/Unit-Testing-Principles-Practices-Patterns
61. Test-Driven Design Insights
Article: https://victorrentea.ro/blog/design-insights-from-unit-testing/
Code: https://github.com/victorrentea/unit-testing.git on branch: devoxx-be-2023
About Victor Rentea: https://victorrentea.ro
Stay connected. Join us:
Editor's Notes
I'm victor rentea from Romania. I'm a java champion, 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 has become 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 various questions 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.