Improve your team’s communication with tests
By: Raphael Adrien
Tags: Software Development, Kotlin, Team Communication, Software Testing, Coding

Your code could be the best way to explain what your code does.

We all know about the importance of good communication. In 1967, Melvin Conway, a computer scientist from the US, wrote an article for the Harvard Business Review titled “How Do Committees Invent?”. This article was the basis of what we now call Conway’s law. It says that “any organization that designs a system will produce a design whose structure is a copy of the organization’s communication structure.” Translating this statement to our world of software development, we could say that expensive and difficult to maintain code is the result of poor communication within the company.

We have also heard of this in the Agile Manifesto. Its first value states, “Individuals and interactions over processes and tools.” In many agile frameworks, the communication problem is solved by creating ceremonies, which are points of contact between the project stakeholders. However, written communication remains inevitable.

Let me give you a practical example: here at tb.lx, we work with teams spread across 4 countries (US, Japan, Portugal, Germany) on 3 continents. We all have different cultures, use different languages, and, most importantly, are located in different time zones, which often makes instant verbal communication impossible, and asynchronous written communication also takes a longer time.

The advantages of good tests

In this reality, tests then become one of the main communication tools amongst developers. With tests, we can:

Communicate the initial intention the code was developed with;

Identify the premises identified and met;

Clearly interpret what the code does;

Guarantee that code conventions are being followed;

The good thing about using tests is that there is no dependency on any specific technology. Instead, it is essential to follow good practices when writing tests:

Good tests should be:

Thorough — they should prove that a behavior is correct;

Stable — they should not break with every change of an implementation detail;

Fast — to have a fast feedback loop;

Few — putting the focus on what needs to be tested, unnecessary tests only increase the codebase and maintenance costs.

Photo by Christopher Gower on Unsplash

The 3 steps to writing good tests

So, how do we write good tests? Let’s split it into three steps:

1. Step: Check the structure

In the first step, we test the structure of the test, checking the dependencies between packages and classes, layers, naming conventions, etc. By doing this, all developers have a common language that explains how the code should be organized. It also guarantees that the standards defined at the beginning of the project, are being followed. Another great advantage is that conflicts over the understanding of how the project should be organized can be avoided since these rules are clear to everyone through the tests.

Within the Java / Kotlin world, we can use ArchUnit to help with this process. It is a very simple library and has good integration with JUnit. Below we have a very simple example of the setup and use:

Add ArchUnit as a dependency on your project.

Maven

<dependency>

<groupId>com.tngtech.archunit</groupId>

<artifactId>archunit</artifactId>

<version>0.13.1</version>

<scope>test</scope>

</dependency>

Gradle

dependencies {

testCompile 'com.tngtech.archunit:archunit:0.13.1'

}

Now we can create a test. All examples in this article are written in Kotlin, which is one of the most used languages in our projects at tb.lx. Kotlin (and for that matter, other languages, e.g., Python too, but not Java) allows us to write the test names as phrases insteadOfCamelCasePhrasesAsTestNames, which makes the test name and intention so much easier to understand.

class CodingRules {



val listClasses = ClassFileImporter().importPackages("tblx.io")



@Test

fun `ensures that classes doesn't throw generic exceptions`() {

NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS.check(listClasses)

}



@Test

fun `ensures that service classes do not access the controller classes`() {

noClasses().that().resideInAPackage("..service..") .should().accessClassesThat().resideInAPackage("..controller..")

.check(listClasses)

}



@Test

fun `ensures that repository classes do not access the service classes`() {

noClasses() .that().resideInAPackage("..repository..") .should().accessClassesThat().resideInAPackage("..service..") .check(listClasses)

}

}

Here (https://github.com/TNG/ArchUnit-Examples) are many examples of how to use ArchUnit.

2. Step: Write proper specifications

The second step is about writing proper test specifications. Always remember that tests should be easy to read and well organized.

To achieve this, we respect the concept of a single expectation test. It helps us in finding possible errors by going straight to the failed test and making the code more readable.

Another powerful tool is Contexts, which makes our tests more precise and more organized. In the Java world, the most used testing framework is JUnit. It was only on version 5 that this platform included Context extensions, and this features really adds value to communication between the teams. See the difference below with and without context.

Without context:

class CarServiceTest {



private val carRepository = mock<CarRepository>()

private val subject = CarService(carRepository)



@Test

fun `when a valid VIN is provided and this number exists in database, returns the vehicle`() {

val vin = "a-valid-vin"



whenever(carRepository.findByVin(vin)).thenReturn("vehicle")



val carRetrieved = subject.retrieveCarBy(vin)



assertEquals("vehicle", carRetrieved)

verify(carRepository, times(1)).findByVin(vin)

}



@Test

fun `when an invalid vin is provided, throw InvalidVinException`() {

val invalidVin = "#"



assertThrows<VinException> {

subject.retrieveCarBy(invalidVin)

}



verifyZeroInteractions(carRepository)

}

}

With context:

class CarServiceTest {



@Nested

@DisplayName("when a valid VIN is provided")

class WhenAValidVINIsProvided {



private val carRepository = mock<CarRepository>()

private val subject = CarService(carRepository)



@Test

fun `and this number exists in database, returns the vehicle`() {

val vin = "a-valid-vin"



whenever(carRepository.findByVin(vin)).thenReturn("vehicle")



val carRetrieved = subject.retrieveCarBy(vin)



assertEquals("vehicle", carRetrieved)

verify(carRepository, times(1)).findByVin(vin)

}

}



@Nested

@DisplayName("when an invalid VIN is provided")

class WhenAnInValidVINIsProvided {



private val carRepository = mock<CarRepository>()

private val subject = CarService(carRepository)



@Test

fun `throws InvalidVinException`() {

val invalidVin = "#"



assertThrows<VinException> {

subject.retrieveCarBy(invalidVin)

}



verifyZeroInteractions(carRepository)

}

}

}

Please note that these tests are much easier to read and making future maintenance easier. This simple details make reading our code much simpler.

3. Step: Write Acceptance Tests

The third and last step is to write Acceptance Tests. Keep in mind that Acceptance Tests are very different from Integration Tests: Integration Tests verify the integration of many units together, while acceptance tests verify that the requirements for a certain feature are met. They are undoubtedly an excellent tool for communicating with your CTO since he gets a tested view of all the features that the system supports.

The most serious difficulty in writing this type of test is to mock calls to external endpoints. In this case, WireMock can make our life easier. WireMock is a library that allows us to mock a server that provides answers according to our use case.

Below is a simple example of using this library. I will not go into detail about it, as this deserves a new article for it.

Add ArchUnit as a dependency on your project.

Maven

<dependency>

<groupId>com.github.tomakehurst</groupId>

<artifactId>wiremock-jre8</artifactId>

<version>2.26.3</version>

<scope>test</scope>

</dependency>

Gradle

testCompile "com.github.tomakehurst:wiremock-jre8:2.26.3"

We can now mock an external request.

class BusServiceTest {



@Nested

@DisplayName(value = "Given that user provide a bus ID")

class GivenUserProvideBusId {



private val wireMockServer = WireMockServer()

private val vehicleId = UUID.randomUUID()



private val subject = BusService()



@BeforeEach

fun init() {

wireMockServer.start()

WireMock.configureFor("localhost", 8080)



WireMock.stubFor(get(urlEqualTo("/vehicle/$vehicleId")).willReturn(

aResponse()

.withStatus(200)

.withBody("a-name"))

)

}



@Test

fun `when the bus service is available, then return the name of vehicle`() {

Assertions.assertEquals("a-name", subject.retrieveBusById(vehicleId))

}



@AfterEach

fun tearDown() {

wireMockServer.stop()

}

}

}

This is my take on how to improve our team’s communication by using tests. Let me know if this helped you in your day-to-day development or if you have other tools and methods.