Test Infinite Scrollers In JavaScript UI
An infinite scroller is a common JavaScript UI tool which we use to gradually show more records on a page as a user scrolls.
It often leaves developers in knots trying to write tests for it because:
- it is hard to encapsulate as a simple input/output based state test
- it seems the only option is to use e2e testing frameworks because it has complex interaction
In order to test one of these components you just need to properly break apart the callback mechanism inside, then test it via a simple unit test that can control it’s state easily.
Now we will look at the react-window-infinite-loader which extends the react-window control. Since this article is generic, we are using this NPM module to demonstrate the overall approach to solving this sort of problem (UI controls that have complex state and interaction you want to test). This can be applied in any UI framework.
Testing Anything
There are usually two approaches to writing tests. The first relies on integration tools to capture what needs to be tested such as cypress and other e2e frameworks. The second breaks apart the code to make the architecture testable.
When we want to use the second approach and make code ‘testable’. Then we need to be able to remove dependencies that will stop us running clean and simple unit tests; without the overhead of integration or e2e testing.
Consider the following infinite scroller function…
How many dependencies does this code have?
Most people will normally say 1, because they can see that we have a reference to this.setState (which is part of the React UI framework). But let’s consider the fragment of HTML that calls it…
Notice the loadNextPage function. This is a dependency the UI frameworks force on us. It is a function that is called by the infinite-loader npm package.
So, this means in its current form we cannot test this because we cannot intercept this call without actually driving this HTML in a test condition. In order to fix problems like this (where we want to call what will be called by framework/tool code) then we need to create a ‘seam’.
Seam
When we talk about the architecture of code a ‘seam’ defines a place in the code where we expect something else to invoke it.
A seam is slightly different to a dependency, because while the dependency exists (in our code example) whether we like it or not. A seam does not. A seam is something we identify, and then… do something about. In our case we decide to open this seam by inserting code in between it (the HTML) and our code (what we want to test). This creates a source-code dependency.
Dependencies
A dependency is either code that is invoked dynamically at runtime using references that are passed around an app, or it is something that has a source-code definition. In the JavaScript world dependencies are normally loaded using import and NPM.
Using a source code dependency we can remove the need to test this code with e2e tests by ‘abstracting’ our code. Code which has done this abstraction could look something like this…
In this example we have moved all the code that actually manipulates data (inside the bindNextPage function) into its own class Presenter. This Presenter could now be put into its own file (the source code dependency). So now we are passing in the runtime dependencies (init, nextPage) meaning we can create our own fake/test versions when we want to run this code in isolation!
We could just create the presenter, and send in our test/fake functions (which behave like the framework – but that we control) like this…
You can see how both tests are very clean and simple, yet they are still exercising our bindNextPage code as a specification making it easy to control.
Moving code around like this is a really important skill if you want to master architecture, why? Because you make it testable!
When you make code testable you immediately improve its architecture.
A good architect will always have code that is modular and re-usable what we could say is ‘independent’. By making the code testable like this you begin getting better architecture, the two go hand-in-hand!
Conclusion
Infinite Scrollers and UI tools like it present an interesting testing challenge because it’s hard to make their state testable, and they usually come deeply buried inside frameworks (in this case React and Infinite Scroller).
This means we must identify a seam and then abstract a dependency in order to control the code we want to test, independent of the framework.
By introducing a dependency via an exposed function or class; we can intercept and inject fake behavior and then isolate the code we want to test using the humble object pattern; this lets us test code simply and predictably instead of having to reach for heavy weight e2e tests all the time!
Comments
Post a Comment