Skip to main content

Search...

Sociable testing

Why are hundreds of unit testers created, but the software still doesn't run? Because isolated tests alone do not prove a feature - and that is the problem.

9 min read
Cover for Sociable testing

Sociable unit tests refer to automated tests that do not test individual classes in isolation, but run through complete use cases and features from the UI entry to the database. Developers are responsible for this. Mocking external systems remains the exception, database access is part of the test.

Key Takeaways

  • Developers are done when the test code that proves the feature is available, not when the production code compiles.
  • Sociable unit tests that puncture complete use cases and features have an undeniable raison d’être; isolated unit tests are a nice-to-have at best.
  • If you mock away the database, you are testing a system that behaves differently than in production and thus lose the validity of the entire test.
  • Test data builders reduce the creation of a business object to a one-liner and lower the inhibition threshold for developers to write tests at all.
  • Design for testability is a conscious architectural decision: Those who purposefully keep their technology stack small can run use case tests directly in the unit testing framework, without browser automation.

Developers are responsible for making sure the software runs

A developer’s job does not end with a collection of functioning individual parts. It only ends when the features and use cases actually work together and produce what the developer is paid for.

This is precisely where a common problem lies. Even teams that accept testing as necessary often write many small tests, at the end of which the software still doesn’t run. The components work in isolation, but their interaction remains untested.

Jan Leßner puts this responsibility in a nutshell: “Developers must ensure that use cases and features work as they were ordered. Proving this automatically is not an optional extra, but part of the actual work.

What are isolated and sociable unit testers?

Martin Fowler distinguishes between two types of unit tests for which developers are responsible: isolated and sociable unit tests. Both are the responsibility of the developer, but they do different things.

Isolated unit testers test a single building block on its own. They are the “nice to have” that developers like to throw themselves into because they are so easy to write. Their blind spot: they ignore the fact that the tested parts have to collaborate in order to produce a feature at all.

Sociable unit testers test larger units. The unit with the indisputable right to exist is a use case or a feature. Those who limit themselves to isolated tests are leaving out the most important thing: proof that the interaction results in the desired behavior.

Those who delegate feature testing away are shifting their responsibility away

The test pyramid remains valid as long as it is not abused. The majority of automated unit tests are in the hands of developers at the bottom, customer acceptance testing at the top and integration testing in between.

The middle layer is the gray area. Originally, “integration testing” meant those tests in which a system is placed in a context and communicates with other subsystems. Today, everything for which a developer has no idea how to write an isolated test is often shifted there.

The classic case: frontend and backend are developed separately and nobody knows how to test them together. So “the test department” does it. As a result, the sociable unit tests move to the top, with the label “not my job”.

This becomes particularly clear with microservices. Anyone who chops up a functionality into many individual services without need and then says they only have to test their own service is delegating away responsibility that used to lie with them. That is a no-go. The responsibility for the interaction remains with the developer.

Headless end-to-end testing bypasses slow UI tools

UI testing tools such as Selenium or Cypress are difficult to use for the average developer. They create their own world of expression, force a lot of redundant code and, above all, are slow. This is exactly what prevents developers from testing entire features.

Headless end-to-end testing is an alternative. Here, the pure representation layer is stripped down completely at the top, i.e. everything that only serves the display. Everything underneath is tested: from the UI logic to the business logic to the database, from top to bottom.

Technically, these tests can be integrated into the usual unit testing framework, such as JUnit. In the project context described, GWT helps because the entire client code is also Java. Everything then runs in a JVM via a simulation of the server interface, just like a normal unit test.

Not every team has this option. However, it is not a pure stroke of luck, but the result of a conscious design decision.

Testability is part of the architecture decision

Design for test means thinking about testability when choosing frameworks and architecture. If you want to make large units testable, you don’t get it for free.

In the aforementioned project, the decision in favor of GWT was initially made for another reason: the team did not want a separation between front-end and back-end developers, but rather full-stack developers. A small, fully manageable stack was the goal. With Java on both sides, good testability was a side effect.

This leads to a general attitude: don’t just take what the sparrows are whistling from the rooftops. The more you limit your options, the more you can ensure that individual aspects of your architecture work really well.

If you’re open on all sides, you’re not completely tight. The more I can limit myself, the more I can focus on making certain aspects work very well.

Jan Leßner

Why “finished development, just need to test” is a trap in thinking

The sentence “I’ve finished development, I just have to test it” contains two traps. Both reveal that someone has yet to make the decisive leap.

The first trap is the assumption that testing comes for free. If you have built the production code but no testers, you are not finished. An iteration is not complete until the test proves that something has been delivered. The alternative would be to say to the customer: “Try it out.” That doesn’t work.

The second pitfall lies in the word “yet”. If you only build tests afterwards, you have developed the production code in slow edit-compile-test cycles: Start up application, click, shut down, start up again. This cycle needs to be interrupted.

If tests are written in parallel to the production code, they already accelerate its development. You get production code and test code in the same time that you would otherwise only have needed for the production code. Prerequisite: Writing tests must be lightweight.

Tests with a real database instead of a mocked core

The principle is: What is an integral part of the system is not mocked away. This includes the database, persistence and messaging systems.

If you mock away the database, you are testing a system that is highly likely to behave differently than in production. Just because there is a socket connection in between, the database is not suddenly foreign. That’s why the tests run with the database. Today, Docker makes it easy to install everything you need locally.

This eliminates the most important reason for mocking frameworks, namely to quickly mock away the database. The question remains as to where mocking is still necessary at all.

Mocks only for real external systems

Mocks belong where software really addresses external systems that do not even exist in development and acceptance testing. A document management system that only exists at the customer is such a case.

There are usually only a handful of these: a DMS, an e-mail server and a few others. With so few places, it pays to build the mocks with more care instead of scattering them across a framework.

A viable pattern controls these mocks via database tables. Certain values are set in the test, and the external system behaves exactly as the test needs it to. The same mock can be reused in acceptance testing, where the real systems are also missing.

Two points speak against mocking frameworks in continuous use: Mock implementations are difficult to understand, mainly because it is rarely stated why a mock does something. And they cost additional implementation effort.

How do you make test data lightweight?

Fast test data determines whether developers enjoy writing tests at all. If you have to put together a complex customer construct for every test, it’s better not to.

The builder pattern is the method of choice. Creating a business object in the database should be a one-liner. In the project described, a line such as Customer.Builder... is sufficient and the customer exists.

If the creation is so fluffy, the motivation is lost. Ideally, a developer says: ‘Starting the application is too tedious for me, I’d rather write a test quickly. If a team reaches this point, the relationship to testing has fundamentally changed.

Slow tests are not destiny

If a test takes 30 seconds to start up, no developer gets into the flow. They check emails, get coffee and lose the thread.

Such slowness is often accepted with a shrug of the shoulders, for example when starting up a Spring container. Spring-given is then considered God-given. Design for testing means not accepting this.

The JVM itself takes about one second. What happens in the remaining 29? This question can be answered with Profiler and Debugger. If you get to the bottom of it, you get the waiting time down to a few seconds, and then testing is fun.

How do you get into Design for Test when the software is already there?

Existing software that was never designed for testability makes it harder to get started, but not impossible. A project with 300,000 lines of code and only 20 meaningless Mockito tests can effectively be treated like starting from scratch.

Don’t drill the thickest plank first. Create foundations that make developers realize that things are suddenly possible. A good first step is a cascaded system configuration: checked-in defaults that a test overwrites programmatically. This gives each developer their own database, and every type of configuration becomes testable.

The next steps almost automatically follow from this bootstrap. If the start time interferes, knock it down. Then come the builders. First by hand, then you notice that their structure is derived one-to-one from the business objects.

Instead of buying a generator framework, a self-built generator is often sufficient: read out the business classes via Java Reflection, write out the code with System.out, copy it into the class, done. Lots of small-step automation that can be done quickly.

A pragmatic way to get started with a new test: first write the line you want to call. Then work backwards to make it just as easy.

Share this page

Related Posts