avatarLuís Soares

Summary

The article argues against unit testing (de)serializers, advocating instead for testing them within the context of their adapter.

Abstract

The article emphasizes that not all components in software development require unit testing, particularly focusing on (de)serializers. It suggests that according to hexagonal architecture principles, (de)serializers are part of the outer layer of an application and should not contain business logic. The author posits that testing (de)serializers directly is testing implementation details, which is unnecessary and can lead to fragile tests. Instead, the author recommends testing (de)serializers indirectly as part of their owner adapter, ensuring that the tests focus on the component's functionality rather than its internal workings. This approach aligns with the concept of black-box testing, where the internal mechanisms are not the focus, but rather the correctness of the output. The article provides examples of how to properly test a web handler that uses JSON serialization without directly testing the serializer itself, thus maintaining a clear separation of concerns and adhering to the principles of vertical decoupling and self-documenting code.

Opinions

  • Directly testing (de)serializers is considered wrong as it emphasizes implementation details and makes tests more fragile.
  • (De)serializers should be private within their adapter to ensure they can change without affecting other parts of the architecture.
  • The DRY principle should not be applied to the extent that it leads to code sharing between different adapters if it's not justified by their evolution.
  • Testing (de)serializers directly is seen as redundant since they are akin to toString methods and should rely on the robustness of the underlying JSON library or runtime.
  • Testing an adapter as a client, rather than its internal (de)serializers, reflects real-life behavior and documents the component's usage more effectively.
  • The author believes that the mindset of "every class needs unit tests" is a fallacy and that testing should focus on components with clients, not on utility classes like serializers.
  • The article suggests that testing implementation details is a common mistake in automated testing and that adapters should be tested as a whole to support refactoring and maintainability.

Stop testing your (de)serializers

One of the goals of automated testing is “testing as a safety net” but that doesn’t mean that you should unit test everything. (De)serialization is an example of something that you should not test directly.

According to the hexagonal architecture, an adapter belongs to the outer layer of your app; it’s the glue between a component and the outside world, whether a database, a gateway for a REST API, your own REST API, etc. Adapters should only deal with parsing, (de)serialization, routing, error handling, and similar; they should never contain any kind of business logic.

Photo by Celia Michon on Unsplash

The need to store and transmit data requires some kind of string representation which is given by serializers (object → string). Deserializers do the opposite (string → object). JSON is just a possible string representation but there are others.

🛑 The wrong way to test (de)serializers

@Test
fun `serialise a customer`() {
   val serializer = UserSerializer()

   val result = serializer.toJson(User("john", "wilson"))

   JSONAssert.assertEquals(
       """{ "name": "john", "lastName": "wilson" }""", result, true
   )
}

Why is it wrong? It puts too much emphasis on implementation details making the tests more fragile. We shouldn’t be able to instantiate UserSerializer from the tests. (De)serializers are adapter details; they should be able to change freely without impacting anything else other than their own adapter. The only way to guarantee it is by making them private within their adapter. This helps to keep JSON concerns where they belong: the outer layers of your architecture and never your domain. It also helps achieve vertical decoupling of use cases.

You might argue that a serializer could be shared across multiple web handlers (e.g. a customer JSON that is used in its detail and its listing) but that just shows an abuse of the DRY principle. The serializers may look the same, but that is only a coincidence since they will probably evolve in different directions. Even if they were shared, that wouldn't free you from properly testing the multiple web handlers separately.

(De)serializers are just a glorified kind of toString and therefore not supposed to have any business logic, only transformation code. Testing them directly is just testing data conversion (object ⭤ JSON). If you’re using some JSON library or the runtime, it almost feels like you’re testing it, which hopefully is thoroughly tested by its developers. Therefore, it doesn’t pay off in terms of improving the test safety net.

✅ The proper way to test (de)serializers

Let’s now consider a way to test (de)serializers in the context of their owner adapter. Being an indirect way, the test coverage remains the same. The difference is that we’ll test an actual component, not some implementation detail. We don’t care how the (de)serialization happens as long as it’s done. Let’s go over an example of testing a web handler to get a list of users in JSON.

In the code excerpt above we have a setup block, where we launch our server with the web handler under test. In the test itself, we prepare a user list stub; then we exercise the SUT as a real client, by actually using an HTTP client and parsing the result. Finally, we assert by checking the generated JSON. Here’s the implementation:

Notice that UserRepresenter is private which tells the reader that its sole purpose lies in the parent class. This is an example of self-documenting code.

Yes, it’s more code than before, but we're now testing the adapter as a client — we can call it the adapter’s unit test. The clear advantage is that we’re able to test and document real-life behavior. You may also argue that the tests are slower this way but there should be very few tests like this, as adapters should not contain business logic at all.

📝 We saw an example of testing our own REST server. If we needed to test a gateway (a REST client for an external service), we’d need a slightly different approach, although the arguments against testing serializers would remain.

tl;dr

Testing implementation details is a common mistake in automated testing. Testing a (de)serializer directly is just an example of that. The fact that I keep seeing it in different projects lead me to write this article. Hexagonal architecture or not, it’s wrong.

Testing a (de)serializer implicates splitting an adapter that shouldn’t have been split from the start. Nobody creates (de)serializers just for the sake of it; there’s always a reason. Therefore, make sure you make them visible only for that reason. This applies to any adapter, whether it belongs to some web handler, gateway, database repository, etc.

What’s a good heuristic to know what to test directly? Well, “everything that has a client” is a good rule of thumb for me. The client can be another software layer, a browser, or even the user. Does a serializer have a client? No. It’s supposed to live inside its adapter as a helper utility as it exists solely to serve it. “Every class needs unit tests” is a fallacy that leads to the wrong mindset regarding automated testing.

What we learned can be applied in other app entry points like web page controllers, and event handlers, among others:

Automated Testing
Serialization
Json
Clean Architecture
Antipattern
Recommended from ReadMedium