Proper abstraction is an important part of writing highly maintainable and usable software. In this article, I aim to address how explicitly testing against abstract data types can help you write more robust tests (that don’t depend on implementation details) and avoid duplicating code.

What is an Abstract Data Type (ADT)?

An abstract data type is a set of values and associated operations that are specified indepent of any concrete implementation.



— Paul E. Black, Dictionary of Algorithms and Data Structures

While the formal definition of an abstract data type may not be extraordinarily intuitive, it is likely that you are familiar with many concrete examples. Stacks, lists, queues, and dictionaries are all common examples of ADT’s.

We can define an immutable Stack ADT in Reason as a module that satisfies the following signature and informally expressed properties

We could also express this ADT using a record type, or with an interface in an object oriented language. These approaches are also valid and can make sense depending on your use case. For the purposes of this article we will be using module types.

How is this relevant to the applications/libraries that I am creating?

While data structures are certainly the most obvious examples of ADT’s, they are in fact quite common in many applications. We will walk through an example that can hopefully be applied more generally. We will be using Rely (which I have spent a fair amount of time working on) to write our tests.

Explicitly Identifying our ADT

When going through this exercise. It is helpful to first explicitly identify both the type of the ADT (which hopefully already exists in some form in your application) as well as the explicit set of rules that we expect to be true of any instance of the ADT. This set of rules will be the basis for our tests cases.

For this example, we define our ADT using the following module type (note that this could certainly be expressed as a record type as well)

Based on this type and our intuitive understanding of what a payment method is, we can develop some plain English rules to describe what we expect to be true of any payment method.

If there are $N in available funds and $M is spent, there should be $(N-M) remaining

Spending more than is available should return an Error

Translating to test cases

Now we can begin to translate the rules into actual code. In Reason, a natural way to build these tests independent of implementation is by using a functor (module function). With other representations/programming languages, ordinary functions and generic classes could be used instead.

Given this functor, we can now easily add test coverage for new payment methods that we might make.

Green Tests!

For the full code example see the accompanying github repository.

I hope that this concept is useful. Please let me know if there are any other testing topics you would like to see my cover by reaching out on twitter (@the_banderson) or the reason discord.

I first learned about testing ADT’s (and much more) from Zoran Horvat’s excellent Pluaralsight course on Writing Highly Maintainable Unit Tests (this is not an affiliate link), which presents similar material from an object oriented perspective using C#.