Catching up with tests in Programmers' diary project
Hi. This is the twenty-first part of the diary about developing the “Programmers’ diary” blog. Previous part: https://hashnode.programmersdiary.com/security-library-and-redis-in-blogging-microservices. The open source code of this project is on https://github.com/TheProgrammersDiary.
The first diary entry explains why this project was done: https://medium.com/@vievaldas/developing-a-website-with-microservices-part-1-the-idea-fe6e0a7a96b5.
Next entry:
—————
2024-01-13
I tried to write tests first and then code, however the desire to add features prevented me from testing an adequate amount of code. Tests need to be written to catch up the development.
Let’s go to Post microservice. It has few endpoints: post creation, post search by id, and retrieval of all posts.
However, the integration test tests only the creation of post.
Let’s add unit tests for all the functionality. Let’s add a test even for post creation since in Building Microservices book Sam Newman advices to add smaller tests which can support bigger ones. The point is that smaller tests allow for quicker debugging since bigger ones often don’t point exactly there the error is (also if the bigger test would be expensive the smaller test launched earlier could provide a developer quick feedback and he could start fixing code seconds after the bug was found).
OK, the three additional tests will require the creation of post. To reduce duplication, we can employ the test mother - a factory for test object (https://martinfowler.com/bliki/ObjectMother.html and https://jonasg.io/posts/object-mother/).
Here is the mother:
public class PostMother {
private final PostController controller;
public PostMother(PostController controller) {
this.controller = controller;
}
public PostRepository.PostEntry create() {
return controller.create(new Post("author", "title", "content")).getBody();
}
public List<PostRepository.PostEntry> createMultiple() {
List<PostRepository.PostEntry> entries = new ArrayList<>();
entries.add(controller.create(new Post("author1", "title1", "content1")).getBody());
entries.add(controller.create(new Post("author2", "title2", "content2")).getBody());
return entries;
}
}
Since we are unit testing, let’s add a fake repository (because Spring will not work in a unit test):
public class FakePostRepository implements PostRepository {
private final Map<String, PostEntry> entries = new HashMap<>();
@Override
public <S extends PostEntry> S save(S entry) {
entries.put(entry.getId(), entry);
return entry;
}
@Override
public <S extends PostEntry> Iterable<S> saveAll(Iterable<S> entries) {
throw new UnsupportedOperationException("Not implemented.");
}
@Override
public Optional<PostEntry> findById(String id) {
return Optional.ofNullable(entries.get(id));
}
@Override
public boolean existsById(String id) {
return entries.get(id) != null;
}
@Override
public List<PostEntry> findAll() {
return entries.values().stream().toList();
}
@Override
public Iterable<PostEntry> findAllById(Iterable<String> ids) {
throw new UnsupportedOperationException("Not implemented.");
}
@Override
public long count() {
return entries.size();
}
@Override
public void deleteById(String id) {
entries.remove(id);
}
@Override
public void delete(PostEntry entry) {
entries.values().remove(entry);
}
@Override
public void deleteAllById(Iterable<? extends String> ids) {
throw new UnsupportedOperationException("Not implemented.");
}
@Override
public void deleteAll(Iterable<? extends PostEntry> ids) {
throw new UnsupportedOperationException("Not implemented.");
}
@Override
public void deleteAll() {
entries.clear();
}
}
Some of these operations are not implemented since we don’t need them right now (YAGNI).
And here are the new unit tests:
@ExtendWith({SnapshotExtension.class})
public class PostTests {
private Expect expect;
private final PostController controller;
private final PostMother mother;
public PostTests() {
this.controller = new PostController(new FakePostRepository());
this.mother = new PostMother(this.controller);
}
@Test
public void createsPost() {
PostRepository.PostEntry post = mother.create();
expect.toMatchSnapshot(jsonWithMaskedProperties(post, "id"));
}
private <T> ObjectNode jsonWithMaskedProperties(T object, String... properties) {
ObjectNode node = new ObjectMapper().valueToTree(object);
Arrays.stream(properties).forEach(property -> node.put(property, "#hidden#"));
return node;
}
@Test
public void findsPost() {
PostRepository.PostEntry initialPost = mother.create();
PostRepository.PostEntry foundPost = controller.getById(initialPost.getId()).getBody();
assertEquals(initialPost, foundPost);
}
@Test
public void findsAllPosts() {
List<PostRepository.PostEntry> initialPosts = mother.createMultiple();
List<PostRepository.PostEntry> foundPosts = new ArrayList(controller.getAll().getBody());
Assertions.assertThat(foundPosts)
.extractingResultOf("toString")
.containsExactlyInAnyOrderElementsOf(
initialPosts.stream().map(PostRepository.PostEntry::toString).toList()
);
}
}
With this change post creation, search by id and retrieval of all posts is properly tested.
To measure test coverage, let’s add jacoco (Java Code Coverage Library) as a plugin:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<configuration>
<excludes>
<exclude>com/evalvis/post/PostRepository$PostEntry.class</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>generate-code-coverage-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<phase>test</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.86</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.86</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
I exclude com/evalvis/post/PostRepository$PostEntry.class (when Java compiles it compiles inner classes with $ symbol, probably to avoid confusing the outer class with a package) to judge only the classes that actually need testing right now. PostEntry does not have logic I want to test.
I also make line and branch coverage requirements at least 86% (90% might be overkill to maintain, but I think we should strive for more than 80%).
The you run e.g. mvn clean test, mvn clean package, mvn clean install, jacoco will generate a coverage report and will fail Maven if coverage is not sufficient.
The coverage report looks like this:
SecurityConfig is also covered since the IT test runs as well.
We have caught up with tests in Post microservice.
—————
Thanks for reading.
The project logged in this diary is open source, so if you would like to code or suggest changes, please visit https://github.com/TheProgrammersDiary.
You can check out the website: https://www.programmersdiary.com/.
There will be more content.
Subscribe to my newsletter
Read articles from Evaldas Visockas directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by