{"id":31,"date":"2025-10-14T11:06:20","date_gmt":"2025-10-14T09:06:20","guid":{"rendered":"https:\/\/rocketzki.com\/?p=31"},"modified":"2025-10-14T11:13:34","modified_gmt":"2025-10-14T09:13:34","slug":"how-can-good-unit-tests-save-your-devs-well-being","status":"publish","type":"post","link":"https:\/\/rocketzki.com\/?p=31","title":{"rendered":"How can good Unit Tests save your Dev\u2019s well-being?"},"content":{"rendered":"\n<p>Imagine you have to implement some business functionality to the application you\u2019re responsible for. Sounds familiar? It may be just a simple thing like <strong>adding timestamp with date zone offset to the db entity<\/strong>. What should be done after a successful implementation of aforementioned feature? <strong>Unit<\/strong> <strong>Tests should be written, of course<\/strong>.<\/p>\n\n\n\n<p>A common mistake, made by beginners especially (by experienced devs as well, of course), is <strong>writing unit tests that test methods and internal logic of the classes<\/strong>. It covers implementation details instead of core business features. It may cause us to miss some not-so-obvious edge cases because we <strong>try to write tests that are aligned to our implementation, not to be compliant to the user story or specification. <\/strong>A well written specification (tests in this case) helps us to fight that problem.<\/p>\n\n\n\n<p><a href=\"https:\/\/web.archive.org\/web\/20230617090355\/https:\/\/cucumber.io\/docs\/bdd\/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>BDD<\/strong> <\/a><strong>\u2013<\/strong> <strong>Behaviour Driven Development <\/strong>has been brought up to life to save us from that kind of unpleasant results. It puts an emphasis on testing real functionalities before testing the dry code of ours (details of implementation).Here is anexample visualizing this error-generating peril.<\/p>\n\n\n\n<p>This app is a little service (could be a part of some micro-service system) which is responsible for receiving events (eg. from service bus) and create or modify user data. Sounds simple but beware, there\u2019s always a catch.<\/p>\n\n\n\n<p>Project structure looks like this. It\u2019s created in <strong>Gradle<\/strong>. Click <a href=\"https:\/\/web.archive.org\/web\/20230617090355\/https:\/\/rocketzki.com\/index.php\/2019\/12\/27\/gradle-adding-dependencies-writing-custom-tasks-how-to\/\" target=\"_blank\" rel=\"noreferrer noopener\">here <\/a>to find out how to manage <strong>Gradle<\/strong> projects.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/web.archive.org\/web\/20230617090355im_\/https:\/\/rocketzki.com\/wp-content\/uploads\/2021\/01\/image-1.png\" alt=\"\" class=\"wp-image-787\"\/><figcaption class=\"wp-element-caption\">Figure 1. Project structure visualized by IntelliJ<\/figcaption><\/figure>\n\n\n\n<p><strong>build.gradle<\/strong> script contains info about dependencies and other project properties.<\/p>\n\n\n\n<p><strong>build.gradle (snippet)<\/strong><\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>(...)\n\ndependencies {\n\ncompile 'org.mockito:mockito-core:2.21.0'\ncompile 'com.fasterxml.jackson.core:jackson-databind:2.9.8'\ncompileOnly 'org.projectlombok:lombok'\nimplementation 'org.slf4j:slf4j-api:1.7.30'\nannotationProcessor 'org.projectlombok:lombok'\ntestImplementation 'junit:junit'\n\n}\n\n(...)<\/code><\/pre>\n\n\n\n<p>Now I am going to explain briefly a responsibility of each class.<\/p>\n\n\n\n<p>Model classes hold information about our <strong>domain<\/strong>. The first one is entity-like class that contains info about our AppUser. That\u2019s pretty straightforward.<\/p>\n\n\n\n<p><strong>AppUser.class<\/strong><\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@Setter\n@Getter\n@NoArgsConstructor\n@AllArgsConstructor\n@Builder\npublic class AppUser {\n    private Long id;\n    private String name;\n    private List&lt;String> assignedApps;\n    private OffsetDateTime userLocalCreatedAt;\n    private OffsetDateTime userLocalUpdatedAt;\n    public AppUser(Long id, String name, List&lt;String> assignedApps, OffsetDateTime timestamp) {\n        this.id = id;\n        this.name = name;\n        this.assignedApps = assignedApps;\n        this.userLocalCreatedAt = timestamp;\n        this.userLocalUpdatedAt = timestamp;\n    }\n}<\/code><\/pre>\n\n\n\n<p>Another model class hold data for events received from outside (eg. aforementioned bus).<\/p>\n\n\n\n<p><strong>AppUserChangeEvent.class<\/strong><\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@Getter\n@AllArgsConstructor\n@NoArgsConstructor\n@Setter\n@Builder\npublic class AppUserChangeEvent {\n    private Long id;\n    private String name;\n    private List&lt;String> assignedApps;\n    private OffsetDateTime timestamp;\n}<\/code><\/pre>\n\n\n\n<p><strong>AdminListener <\/strong>class is a pseudo message listener. I will imitate <em>receive<\/em> method invocation just like there was some queue\/bus attached to it. It is responsible for intercepting event, parsing it using <strong>ObjectMapper <\/strong>and sending to <strong>UserAdminProcessor<\/strong> for further processing.<\/p>\n\n\n\n<p><strong>AdminListener.class<\/strong><\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class AdminListener {\n    private final UserAdminProcessor userAdminProcessor;\n    private final ObjectMapper objectMapper;\n    public AdminListener(UserAdminProcessor processor, ObjectMapper objectMapper) {\n        this.userAdminProcessor = processor;\n        this.objectMapper = objectMapper;\n    }\n    @SneakyThrows\n    public void receive(String message) {\n        try {\n            var appUser = objectMapper.readValue(message, AppUserChangeEvent.class);\n            userAdminProcessor.process(appUser);\n        } catch (IOException exc) {\n            System.out.println(\"Cannot read message\");\n            throw exc;\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p><strong>UserAdminProcessor<\/strong> contains business logic code which \u2018decides\u2019 what to do with the event it has received. <strong>AppUser <\/strong>can be either modified or created, depending on its existence in the database.<\/p>\n\n\n\n<p><strong>UserAdminProcessor.class<\/strong><\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class UserAdminProcessor {\n    private final InMemoryAppUserRepo repository;\n    public UserAdminProcessor(InMemoryAppUserRepo repository) {\n        this.repository = repository;\n    }\n    void process(AppUserChangeEvent event) {\n        var user = repository.findUser(event.getId());\n        if (user != null) {\n            modifyUser(event, user);\n        } else {\n            createUser(event);\n        }\n    }\n    private void modifyUser(AppUserChangeEvent event, AppUser user) {\n        user.getAssignedApps().addAll(event.getAssignedApps());\n        user.setName(event.getName());\n        user.setUserLocalUpdatedAt(event.getTimestamp());\n        repository.save(user);\n        System.out.println(\"## User \" + user.getId() + \" has been modified.\");\n    }\n    private void createUser(AppUserChangeEvent event) {\n        var user = new AppUser(event.getId(), event.getName(), event.getAssignedApps(), event.getTimestamp());\n        repository.save(user);\n        System.out.println(\"## User \" + user.getId() + \" has been modified.\");\n    }\n}<\/code><\/pre>\n\n\n\n<p>The pseudo-repository <strong>InMemoryAppUserRepo<\/strong> holds a standards <strong>HashMap <\/strong>and some convenience methods for saving and retrieving objects from the map and therefore imitates real db repository which we know very well from Spring Data or DAO pattern.<\/p>\n\n\n\n<p><strong>InMemoryAppUserRepo.class<\/strong><\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class InMemoryAppUserRepo {\n    private final Map&lt;Long, AppUser> store = new HashMap&lt;>();\n    public void save(AppUser entity) {\n        putOrReplace(entity);\n        System.out.println(\"### Entity '\" + entity + \"' saved!\");\n    }\n    public AppUser findUser(Long id) {\n        return store.getOrDefault(id, null);\n    }\n    private void putOrReplace(AppUser entity) {\n        if (findUser(entity.getId()) == null) {\n            store.put(entity.getId(), entity);\n        } else {\n            store.replace(entity.getId(), entity);\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p id=\"block-c11c87fc-3de7-4a85-a4f4-8d143eb7fb34\">That\u2019t it for the classes description. How does the data flow works and look like though? The figure should answer that question.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Now let\u2019s run our unit tests!<\/strong><\/h3>\n\n\n\n<p>I am using junit unit test framework to run this thing up.<\/p>\n\n\n\n<p>Common and not-so-correct \u2018tradition\u2019 is testing each class separately and treating each method as a \u2018unit\u2019 in question. As I mentioned in the introduction, it may cause us to miss some crucial bugs. In the test class below I\u2019m presenting such an example. Test are passing smoothly. But are we sure our code is <strong>really <\/strong>correct?<\/p>\n\n\n\n<p><strong>NotEntirelyCorrectUnitTesting.class<\/strong><\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@RunWith(MockitoJUnitRunner.class)\npublic class NotEntirelyCorrectUnitTestingTest {\n\n\n    @Mock\n    private InMemoryAppUserRepo repo;\n\n    @InjectMocks\n    private UserAdminProcessor processor;\n\n    @Captor\n    private ArgumentCaptor&lt;AppUser> appUserArgumentCaptor;\n\n\n    @Test\n    public void userPropertiesAreSuccessfullyUpdated() {\n        given(repo.findUser(9L))\n                .willReturn(AppUser.builder()\n                        .id(9L)\n                        .name(\"TEST_USER\")\n                        .assignedApps(new ArrayList&lt;>(Arrays.asList(\"GameFactor\", \"AdobeXD\")))\n                        .userLocalCreatedAt(OffsetDateTime.parse(\"2020-08-22T11:26:09.00023+01:00\"))\n                        .userLocalUpdatedAt(OffsetDateTime.parse(\"2020-09-23T23:21:34.45292+01:00\"))\n                        .build());\n\n\n        \/\/when listener receives a change event\n        processor.process(AppUserChangeEvent.builder()\n                .id(9L)\n                .name(\"TEST_USER\")\n                .assignedApps(Arrays.asList(\"Notepad\", \"vi\"))\n                .timestamp(OffsetDateTime.parse(\"2020-09-28T18:21:34.45292+01:00\"))\n                .build());\n\n\n        \/\/then a correctly modified entity should be passed to the repository to be saved\n        then(repo)\n                .should()\n                .save(appUserArgumentCaptor.capture());\n\n        assertThat(appUserArgumentCaptor.getAllValues())\n                .hasSize(1)\n                .extracting(\n                        AppUser::getId,\n                        AppUser::getName,\n                        user -> user.getAssignedApps().size(),\n                        AppUser::getUserLocalCreatedAt,\n                        AppUser::getUserLocalUpdatedAt\n                )\n                .containsExactly(\n                        tuple(\n                                9L,\n                                \"TEST_USER\",\n                                4,\n                                OffsetDateTime.parse(\"2020-08-22T11:26:09.00023+01:00\"),\n                                OffsetDateTime.parse(\"2020-09-28T18:21:34.45292+01:00\")\n\n                        )\n                );\n\n\n    }\n\n    @Test\n    public void userIsSuccessfullyCreated() {\n        given(repo.findUser(1L))\n                .willReturn(null);\n\n\n        \/\/when listener receives a change event\n        processor.process(AppUserChangeEvent.builder()\n                .id(1L)\n                .name(\"TEST_USER2\")\n                .assignedApps(Arrays.asList(\"Photoshop\", \"Eclipse\", \"Calc\"))\n                .timestamp(OffsetDateTime.parse(\"2020-09-22T18:29:39.23023+01:00\"))\n                .build());\n\n\n        \/\/then a correctly modified entity should be passed to the repository to be saved\n        then(repo)\n                .should()\n                .save(appUserArgumentCaptor.capture());\n\n        assertThat(appUserArgumentCaptor.getAllValues())\n                .hasSize(1)\n                .extracting(\n                        AppUser::getId,\n                        AppUser::getName,\n                        AppUser::getAssignedApps,\n                        AppUser::getUserLocalCreatedAt,\n                        AppUser::getUserLocalUpdatedAt\n                )\n                .containsExactly(\n                        tuple(\n                                1L,\n                                \"TEST_USER2\",\n                                Arrays.asList(\"Photoshop\", \"Eclipse\", \"Calc\"),\n                                OffsetDateTime.parse(\"2020-09-22T18:29:39.23023+01:00\"),\n                                OffsetDateTime.parse(\"2020-09-22T18:29:39.23023+01:00\")\n                        )\n                );\n    }\n}<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>So how should it be?<\/strong><\/h4>\n\n\n\n<p><strong>UserAdministration.class<\/strong><\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@RunWith(MockitoJUnitRunner.class)\npublic class UserAdministrationTest {\n\n\n    private AdminListener listener;\n\n    @Mock\n    private InMemoryAppUserRepo repo;\n\n    @InjectMocks\n    private UserAdminProcessor processor;\n\n    @Captor\n    private ArgumentCaptor&lt;AppUser> appUserArgumentCaptor;\n\n    @Before\n    public void setup() {\n        listener = new AdminListener(processor, new ObjectMapper()\n                .findAndRegisterModules());\n    }\n\n    @Test\n    public void userPropertiesAreSuccessfullyUpdated() {\n        given(repo.findUser(9L))\n                .willReturn(AppUser.builder()\n                        .id(9L)\n                        .name(\"TEST_USER\")\n                        .assignedApps(new ArrayList&lt;>(Arrays.asList(\"GameFactor\", \"AdobeXD\")))\n                        .userLocalCreatedAt(OffsetDateTime.parse(\"2020-08-22T11:26:09.00023+01:00\"))\n                        .userLocalUpdatedAt(OffsetDateTime.parse(\"2020-09-21T18:21:34.45292+01:00\"))\n                        .build());\n\n\n        \/\/when listener receives a change event\n        listener.receive(\"{\" +\n                \"  \\\"id\\\": \\\"9\\\",\" +\n                \"  \\\"name\\\": \\\"TEST_USER\\\",\" +\n                \"  \\\"assignedApps\\\": &#91;\" +\n                \"    \\\"Notepad\\\",\" +\n                \"    \\\"MediaCoder\\\"\" +\n                \"  ],\" +\n                \"  \\\"timestamp\\\": \\\"2020-10-09T12:56:09.90023+01:00\\\"\" +\n                \"}\");\n\n\n        \/\/then a correctly modified entity should be passed to the repository to be saved\n        then(repo)\n                .should()\n                .save(appUserArgumentCaptor.capture());\n\n        assertThat(appUserArgumentCaptor.getAllValues())\n                .hasSize(1)\n                .extracting(\n                        AppUser::getId,\n                        AppUser::getName,\n                        s -> s.getAssignedApps().size(),\n                        AppUser::getUserLocalCreatedAt,\n                        AppUser::getUserLocalUpdatedAt\n                )\n                .containsExactly(\n                        tuple(\n                                9L,\n                                \"TEST_USER\",\n                                4,\n                                OffsetDateTime.parse(\"2020-08-22T11:26:09.00023+01:00\"),\n                                OffsetDateTime.parse(\"2020-10-09T12:56:09.90023+01:00\")\n\n                        )\n                );\n    }\n\n    @Test\n    public void userIsSuccessfullyCreated() {\n        given(repo.findUser(1L))\n                .willReturn(null);\n\n\n        \/\/when listener receives a change event\n        listener.receive(\"{\" +\n                \"  \\\"id\\\": \\\"1\\\",\" +\n                \"  \\\"name\\\": \\\"TEST_USER2\\\",\" +\n                \"  \\\"assignedApps\\\": &#91;\" +\n                \"    \\\"Photoshop\\\",\" +\n                \"    \\\"Eclipse\\\",\" +\n                \"    \\\"Calc\\\"\" +\n                \"  ],\" +\n                \"  \\\"timestamp\\\": \\\"2020-09-22T18:29:39.23023+01:00\\\"\" +\n                \"}\");\n\n\n        \/\/then a correctly modified entity should be passed to the repository to be saved\n        then(repo)\n                .should()\n                .save(appUserArgumentCaptor.capture());\n\n        assertThat(appUserArgumentCaptor.getAllValues())\n                .hasSize(1)\n                .extracting(\n                        AppUser::getId,\n                        AppUser::getName,\n                        AppUser::getAssignedApps,\n                        AppUser::getUserLocalCreatedAt,\n                        AppUser::getUserLocalUpdatedAt\n                )\n                .containsExactly(\n                        tuple(\n                                1L,\n                                \"TEST_USER2\",\n                                Arrays.asList(\"Photoshop\", \"Eclipse\", \"Calc\"),\n                                OffsetDateTime.parse(\"2020-09-22T18:29:39.23023+01:00\"),\n                                OffsetDateTime.parse(\"2020-09-22T18:29:39.23023+01:00\")\n                        )\n                );\n    }\n}<\/code><\/pre>\n\n\n\n<p>As we can see I am testing an entire flow for two separate cases: <strong>there\u2019s a user already<\/strong> so modify properties and the other: <strong>there\u2019s no user so create one<\/strong>. But in this case the test cases don\u2019t pass. Why?<\/p>\n\n\n\n<p>Take a look at the timestamps that re send as a message to the listener. It is parsed by the <strong>ObjectMapper <\/strong>which converts the date-time to be adjusted to local server time (eg. 11:09:00.33+01:00 is converted to 10:09:00.33Z \u2013 <strong>UTC <\/strong>time). <strong>It shouldn\u2019t happen<\/strong> because we\u2019re using <strong>OffsetDateTime <\/strong>and we want to have the exact date that has been sent to our service \u2013 <strong>that\u2019s why the event class contains timestamp field at all. <\/strong>We need user\u2019s local time with relative to us (our server), <strong>not UTC for that user<\/strong>. That kind of issue could be easily detected by the feature driven tests. So all we need to do is to change <strong>ObjectMapper <\/strong>configuration so it doesn\u2019t adjust any temporal values.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>   @Before\n    public void setup() {\n        listener = new AdminListener(processor, new ObjectMapper()\n                .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false)\n                .findAndRegisterModules());\n    }<\/code><\/pre>\n\n\n\n<p>Now all the tests pass and we have succesfully detected a potentially hamrful bug.<\/p>\n\n\n\n<p><strong>Full code is available at my github repo <a href=\"https:\/\/web.archive.org\/web\/20230617090355\/https:\/\/github.com\/rocketzki\/bdd-example\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/github.com\/rocketzki\/bdd-example<\/a><\/strong><\/p>\n\n\n\n<p>I have showed you a case study concerning the importance of functionality testing. Additionally, you did have a chance to code a simple micro-service which could be serving a real business case (after adding a real framework support of course :)).<\/p>\n\n\n\n<p>Hope you liked it.<\/p>\n\n\n\n<p>In case of any questions and\/or remarks don\u2019t hesitate commenting or reaching me out!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Imagine you have to implement some business functionality to the application you\u2019re responsible for. Sounds familiar? It may be just a simple thing like adding timestamp with date zone offset to the db entity. What should be done after a successful implementation of aforementioned feature? Unit Tests should be written, of course. A common mistake, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":32,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-31","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-bez-kategorii"],"_links":{"self":[{"href":"https:\/\/rocketzki.com\/index.php?rest_route=\/wp\/v2\/posts\/31","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rocketzki.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rocketzki.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rocketzki.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rocketzki.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=31"}],"version-history":[{"count":2,"href":"https:\/\/rocketzki.com\/index.php?rest_route=\/wp\/v2\/posts\/31\/revisions"}],"predecessor-version":[{"id":35,"href":"https:\/\/rocketzki.com\/index.php?rest_route=\/wp\/v2\/posts\/31\/revisions\/35"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rocketzki.com\/index.php?rest_route=\/wp\/v2\/media\/32"}],"wp:attachment":[{"href":"https:\/\/rocketzki.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=31"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rocketzki.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=31"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rocketzki.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=31"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}