Testing Declaratively
By: Cody Towstik
Tags: Programming, Declarative Programming, Guides And Tutorials, Testing, Code

A Practical Approach

So, how far do we take the general principles covered in What is ‘Testing Declaratively’ and synthesize them into a practical approach?

It would be unreasonable to abstract every little action, especially in a complex application. Additionally, it isn’t enough to just extract duplicate code. Let’s take a wholistic look at how we can keep our tests lookin’ fresh.

Here is the original implementation fully refactored declaratively, the analysis is below.

Put simply, we are trying to make our tests robust by making it hard to introduce bugs, and easy to identify existing ones.

Abstractions

Abstractions should be made anywhere where there is duplicated code, taking enumerated parameters when necessary.

For each atomic action, you might want a separate function for each:

navigateToAll()



navigateToFavorites()



navigateToTrash()

S’pose you want to add functionality to click the left or right of an element, each of those functions could take an optional parameter.

navigateToAll(‘left’) navigateToFavorites(‘right’) navigateToTrash(‘center’)

Assertions and similar code should be clear as to what they are asserting, and take an expected and actual value.

testIDShouldContain(TestIDEnum.NOTE_OPTION_TRASH, TextEnum.MOVE_TO_TRASH) assertNoteListLengthEquals(1)

Separate out abstractions that are used by all tests into one file, and consider a logical way to organize shared abstractions across smaller subsets of tests.

Enumerations

Inevitably, we can’t (and shouldn’t) avoid implementation details from every single test — testIDShouldContain(TestIDEnum.NOTE_OPTION_TRASH, TextEnum.MOVE_TO_TRASH) .

As a standard practice, you should already have all of your programs’ string literals enumerated in a separate properties file. Go ahead and do the same for each data-testid .

Using Typescript this might look something like the following. For a native javascript implementation, just store each property key pair in a map.

Similarly to when we moved code into methods, moving strings into enumerations gives developers auto-complete, making tests that much more fun to write. IDEs typically offer syntax highlighting for unused properties. So long as DOM elements are each consistently given an enumerated data-testid , it will be painfully apparent if any are unused, and therefore untested.

Since all instances of string literals should now be replaced by these enumerations, we won’t have to do any searching if we change any of the values. Convenient, robust, and easily debuggable.

As an aside, when naming properties, it can be helpful to have a naming scheme that goes broader descriptor to more specific. It alphabetizes nicely, and make it easy to find related properties. If you are only looking for note menu options, you could type NOTE_OPTION and the desired auto-complete suggestions would pop up.

A Final Word

You’ve probably heard of functional programming, or have encountered it incidentally at some point in your software adventures. One of the major appeals to functional programming is it calls for a declarative approach, which garners the same benefits we have reviewed in this Article. Most modern languages have some implementation of the standardized ‘higher order functions’.

You can see that the code is easy to read, and everyone is speaking the same language. The behaviors of map , filter , and reduce are well documented and agreed upon.

Don’t try to memorize what we have discovered throughout this process as a line item list. Rather, consider the purpose behind each guideline and what issues each is trying to solve. Use these principles as a way to think about not only testing your code, but throughout other facets of your life as a developer.