Sociable unit tests are automated tests that don’t check individual classes in isolation. They run through complete use cases and features, from the entry point in the UI down to the database. Developers are responsible for them. Mocking external systems stays the exception, and database access is part of the test.
Key Takeaways
- Developers are done when the test code that proves the feature exists, not when the production code compiles.
- Sociable unit tests that cut through complete use cases and features have an undeniable reason to exist; isolated unit tests are a nice-to-have at best.
- If you mock the database away, you’re testing a system that behaves differently from production, and the whole test loses its meaning.
- Test data builders turn creating a business object into a one-liner and make developers far more willing to write tests at all.
- Design for testability is a deliberate architecture decision: if you keep your technology stack small on purpose, you can run use case tests directly in the unit test framework, without browser automation.
Developers Are Responsible for Software That Works
A developer’s job doesn’t end with a collection of parts that each work on their own. It only ends when the features and use cases actually work together and produce what the developer is being paid for. Sociable unit tests are how you prove that.
This is where a common problem sits. Even teams that accept testing as necessary often write lots of small tests, and at the end the software still doesn’t run. The components work in isolation, but nobody has checked how they work together.
Jan Leßner puts this responsibility plainly: developers have to make sure that use cases and features work the way they were ordered. Proving that with automated tests is part of the actual work, not an optional extra.
What Are Sociable Unit Tests and How Do They Differ from Isolated Ones?
Martin Fowler distinguishes two kinds of unit tests that developers are responsible for: solitary, or isolated, unit tests and sociable unit tests. Both are the developer’s job, but they do different things.
Isolated unit tests check a single building block on its own. They’re the “nice to have” that developers love, because they’re so easy to write. Their blind spot: they ignore the fact that the tested parts have to collaborate to produce a feature at all.
Sociable unit tests check larger units. The unit with an undeniable reason to exist is a use case or a feature. If you limit yourself to isolated tests, you leave out the most important part: proof that the parts together produce the behavior you want. That’s the idea behind sociable unit testing.
Delegating Feature Tests Means Delegating Responsibility
The test pyramid still holds, as long as it isn’t misused. At the bottom, the bulk of automated unit tests in developers’ hands. At the top, the customers’ acceptance tests. In between, the integration tests.
That middle layer is the gray zone. Originally, “integration test” meant tests in which a system is placed in a context and communicates with other subsystems. Today, everything tends to get pushed there that a developer can’t figure out how to test in isolation.
The classic case: frontend and backend are developed separately, and nobody knows how to test them together. So “the test department” does it. The sociable unit tests move up the pyramid with a “not my job” label attached.
Microservices make this especially visible. If you chop a piece of functionality into lots of separate services for no good reason and then say you only need to test your own service, you’re handing off responsibility that used to be yours. That’s a no-go. Responsibility for how the pieces work together stays with the developer.
Headless End-to-End Tests Get Around Slow UI Tools
UI test tools like Selenium or Cypress are hard to handle for the average developer. They come with their own way of expressing things, force a lot of redundant code and, above all, they’re slow. That’s exactly what keeps developers from testing whole features.
Headless end-to-end tests are an alternative. The pure presentation layer at the top, everything that only serves the display, is stripped away completely. Everything underneath gets tested: from the UI logic through the business logic down to the database, top to bottom.
Technically, these tests fit into the usual unit test framework, JUnit for example. In the project described here, GWT helps, because all the client code is Java too. With a simulated server interface, everything runs in a single JVM, just like an ordinary unit test.
Not every team has that option. Nor is it pure luck: it comes from a deliberate design decision.
Testability Belongs in the Architecture Decision
Design for testability means thinking about testability when you choose frameworks and architecture. If you want to make large units testable, it won’t come for free.
In this project, GWT was chosen for a different reason at first: the team didn’t want a split between frontend and backend developers, it wanted full-stack developers. A small stack that the team could fully master was the goal. With Java on both sides, good testability came as a side effect.
That leads to a general attitude: don’t just go with whatever everyone else is using. The more you narrow your options, the better you can make sure that particular aspects of your architecture work really well.
“If you’re open in every direction, your brain may fall out. The more I can limit myself, the better I can focus on making certain aspects work really well.”
(Jan Leßner)
Why “Development Is Done, I Just Need to Test” Is a Mental Trap
The sentence “I’ve finished developing, I just need to test” contains two mental traps. Both show that someone hasn’t yet made the decisive leap.
The first trap is assuming that tests come for free. If you’ve built the production code but no tests, you’re not done. An iteration is only finished when a test proves that something has been delivered. The alternative would be telling the customer: “Just try it.” That doesn’t work.
The second trap is in the word “just”. If you only write tests afterwards, you developed the production code in slow edit-compile-test cycles: start the application, click, shut it down, start it again. That loop needs to be broken.
When you write tests alongside the production code, they already speed up its development. You get production code and test code in the time you would otherwise have spent on the production code alone. The prerequisite: writing tests has to be lightweight.
Test Against a Real Database, Don’t Mock the Core
The principle: anything that is an integral part of the system doesn’t get mocked away. That includes the database, persistence and messaging systems.
If you mock away the database, you’re very likely testing a system that behaves differently from production. Just because there’s a socket connection in between doesn’t make the database a foreign system. So the tests run against the database. These days, Docker makes it easy to install everything you need locally.
That removes the most important reason for mocking frameworks, namely mocking the database away quickly. Which leaves the question of where mocking is still needed at all.
Mocks Only for Truly External Systems
Mocks belong where software talks to genuinely external systems that don’t even exist during development and acceptance testing. A document management system that only exists at the customer’s site is one example.
There are usually only a handful of those: a DMS, an email server, a few others. With so few of them, it pays to build the mocks carefully by hand instead of scattering them around with a framework.
A pattern that works well controls these mocks through database tables. The test sets certain values, and the external system behaves exactly the way the test needs. The same mock can be reused in acceptance testing, where the real systems are missing as well.
Two things speak against using mocking frameworks all the time. Mock implementations are hard to understand, mostly because they rarely say why a mock does what it does. And they cost extra implementation effort.
How Do You Make Test Data Lightweight?
How quickly you can set up test data decides whether developers enjoy writing tests at all. If you have to assemble an elaborate customer structure for every test, you’d rather not bother.
The builder pattern is the tool of choice. Creating a business object in the database should take one line. In the project described here, a line like Customer.Builder... is enough, and the customer exists.
Once creating data is that easy, motivation flips. Ideally, a developer says: starting the application is too much hassle, I’d rather quickly write a test. When a team gets to that point, its relationship with testing has turned around completely.
Slow Tests Aren’t Fate
If a test takes 30 seconds to start, no developer gets into flow. They check their email, grab a coffee and lose the thread.
This kind of slowness is often accepted with a shrug, for example when a Spring container starts up. “Spring-given” then counts as God-given. Design for testability means refusing to accept that.
The JVM itself needs about a second. What happens in the other 29? You can answer that with a profiler and a debugger. If you get to the bottom of it, you bring the wait down to a few seconds, and then testing becomes fun.
Getting Started with Design for Testability in Existing Software
Existing software that was never designed for testability makes getting started harder, but not impossible. A project with 300,000 lines of code and only 20 meaningless Mockito tests is effectively a project starting from zero.
Don’t go for the hardest problem first. Lay foundations that make developers notice that suddenly things are possible. A good first step is a cascading system configuration: checked-in defaults that a test overrides programmatically. That way, every developer gets their own database, and every kind of configuration becomes testable.
From that bootstrap, the next steps almost suggest themselves. If startup time gets in the way, you whittle it down. Then come the builders. First you write them by hand, then you notice that their structure follows one to one from the business objects.
Instead of buying a generator framework, a homemade generator is often enough: read the business classes with Java Reflection, print the code with System.out, paste it into the class, done. Lots of small automation steps that are quick to build.
A pragmatic way to start a new test: first write down the one line showing how you’d like the call to look. Then work backwards until it really is that easy.
Frequently Asked Questions
Are isolated unit tests sufficient to validate a feature?
No. Isolated unit tests examine a single component on its own and ignore the fact that the parts must collaborate to produce a feature in the first place. At best, they’re a nice-to-have because they’re so easy to write. The unit with an indisputable raison d’être is the use case or feature, tested from the UI entry point all the way to the database.
Who should be responsible for testing the interaction between the front end and back end?
The developer. The test pyramid places the bulk of automated unit tests at the bottom, in the hands of developers, and customer acceptance testing at the top. The integration layer in between becomes a gray area as soon as everything for which no one has a testing idea ends up there. Anyone who unnecessarily breaks functionality down into many services and then tests only their own is shifting responsibility away.
Is it okay to mock out the database in automated tests?
No. Anything that is an integral part of the system should not be replaced: the database, persistence, and messaging. Just because there’s a socket connection in between doesn’t mean the database is suddenly external. If you mock it out, you’re testing a system that will most likely behave differently than it does in production. Docker makes it easy to install what you need locally.
Why do UI testing tools discourage developers from testing entire features?
Tools like Selenium or Cypress create their own language, force a lot of redundant code, and, above all, are slow. Headless end-to-end testing offers an alternative: the pure presentation layer is stripped away, and everything beneath it is tested, from the UI logic through the business logic all the way down to the database. Such tests run within the familiar unit testing framework, such as JUnit.
How does the choice of technology stack affect testability?
A small, fully controllable stack makes large units testable in the first place. In the project described, GWT was chosen because the team wanted full-stack developers instead of separate front-end and back-end roles. Since the client code is also written in Java, the use-case test runs via a simulated server interface within a JVM. Good testability was a side effect of this limitation.
Why do you even need mocks anymore if the database remains real?
For real external systems that don’t exist at all during development and acceptance testing, such as a customer’s document management system or an email server. These are usually just a handful of cases where careful manual work is worth the effort. A robust pattern controls these mocks via database tables, so that the same mock can be reused in acceptance testing.
What should you do if a test takes 30 seconds to start up?
Measure it instead of accepting it. The JVM itself takes about one second, so you need to use a profiler and debugger to figure out where the remaining 29 go. Long startup times for a Spring container aren’t inevitable. If you get to the bottom of it, you can reduce it to just a few seconds. With a 30-second wait, the developer loses focus.
Do you need a framework to generate test data?
Usually not. A homemade generator is often sufficient: use Java Reflection to read the business classes, output the code, and incorporate it into the class. The goal is the Builder pattern, which turns creating a business object in the database into a single line of code. Only when it’s that lightweight will developers prefer to write a test rather than start the application.


