visit
This is the second story of Road to Simplicity. And it’s about the role of tests in software writing.
(The first part is about the goal of the series and hexagonal architecture)
I think that it’s inappropriate to associate test with verification of correctness. After all with a test we can verify that a software module (e.g. a function) returns an expected output with a given input. But we cannot prove correctness in this way. We can prove correctness mathematically. And this is what we do to prove algorithms correctness.
We can see tests as experiments on the software system. And the below quote expresses concisely the above concept:So, at most, a test could prove wrongness…No amount of experimentation can ever prove me right;
Albert Einstein
a single experiment can prove me wrong
Software aren’t static system. They are in evolution. We can add or
delete a feature. We can change its structure. And it changes according the solved problem. There are a lot of reason to change.
However, with an evolution, we can break something that worked in the past. This event is called regression. Eventually we can find regressions manually. But this is a bad road because we could change:
Tests free us from the fear of change. Indeed if a test fail we can investigate and fix the issues. Otherwise we can rest assured: we didn’t break something that worked in the past. This implies code malleability.
This is the most evident advantage. But there is another subtler.The goal of Road to Simplicity is to express a methodology to reach simplicity. And there is a connection with tests.
In the last story we already defined a use case. In reality I write use case and its test simultaneously.These tests are crucial because use cases represents what our software does. And they guarantees simplicity because when we write tests we’re the client of ourselves. And, as programmers, we love simple and clean interface. Furthermore, because test is more code, we don’t want to write unnecessary lines. In this way we are forced to write the most minimal and simplest interface.In the previous part we expressed our use case (GetCapabilitiesUseCase) as:
public interface GetCapabilitiesUseCase {
Single<Capabilities> getCapabilities();
}
class GetCapabilitiesUseCaseTest {
@Test
void ok() throws Throwable {
String javaVersion = randomString();
Long networkSpeed = randomLong();
GetCapabilitiesUseCase useCase = UseCaseFactory.getCapabilitiesUseCase(
() -> Single.just(javaVersion),
() -> Single.just(networkSpeed)
);
TestObserver<Capabilities> useCaseObserver = useCase
.getCapabilities()
.test();
assertTrue(useCaseObserver.await(100, TimeUnit.MILLISECONDS));
useCaseObserver
.assertResult(new Capabilities(javaVersion, networkSpeed));
}
private String randomString() {
return UUID.randomUUID().toString();
}
private Long randomLong() {
return ThreadLocalRandom.current().nextLong();
}
}
This is possible because the Capabilities class allows these values. So I improved its constructor with an exception:
@Value
@Builder
public class Capabilities {
public Capabilities(String javaVersion, Long networkSpeed) {
this.javaVersion = javaVersion;
this.networkSpeed = networkSpeed;
if (this.networkSpeed < 0L) throw new IllegalArgumentException("Network speed should be greater than 0!");
}
private final String javaVersion;
private final Long networkSpeed;
}
Then I added the test class about Capabilities:
class CapabilitiesTest {
@Test
void with_negative_network_speed() throws Throwable {
String javaVersion = randomString();
Long networkSpeed = randomNegativeLong();
try {
new Capabilities(javaVersion, networkSpeed);
throw new IllegalStateException("Invalid capabilities built!");
} catch (IllegalArgumentException e) {
}
}
private String randomString() {
return UUID.randomUUID().toString();
}
private Long randomNegativeLong() {
return ThreadLocalRandom.current().nextLong(1L, Long.MAX_VALUE) * -1L;
}
}
class GetCapabilitiesUseCaseTest {
@Test
void ok() throws Throwable {
String javaVersion = randomString();
Long networkSpeed = randomNonNegativeLong();
GetCapabilitiesUseCase useCase = UseCaseFactory.getCapabilitiesUseCase(
() -> Single.just(javaVersion),
() -> Single.just(networkSpeed)
);
TestObserver<Capabilities> useCaseObserver = useCase
.getCapabilities()
.test();
assertTrue(useCaseObserver.await(100, TimeUnit.MILLISECONDS));
useCaseObserver
.assertResult(new Capabilities(javaVersion, networkSpeed));
}
private String randomString() {
return UUID.randomUUID().toString();
}
private Long randomNonNegativeLong() {
return ThreadLocalRandom.current().nextLong(0L, Integer.MAX_VALUE);
}
}
Stay tuned! :D