Last time, we talked about abstraction on the other side. When tests cannot treat a system as a black box, they need to look inside. Or behind. Once that happens, the abstraction is broken. This time, I want to talk about a specific kind of testing – UI component testing, which may look like unit testing, but because of abstraction issues, can lead to test failure, and not the kind we like. We still need to understand the other side.
Plain Ol’ UI Component Testing
For years in desktop applications, checking components was simple (but not easy). In order to test a component, you treated it like an object, with interface and state. If you wanted to test it in isolation from its environment, you needed to provide it with a test container.
For example, if you had a component including a label, text box and a button, you’d put that on a form, that would be its container. Then you’d locate and operate the internal controls, and check their effect on either the component or the container.
Why simple? Because if the button changed its label, and you wanted to check the new label, you’ll find that button, and ask what its label property is. Or query the component, if it exposed it.
If it was exposed through methods, properties and events, we don’t need to look at it. Simple state checking, which we really like in tests. Because that’s abstraction at its best – only exposing what’s needed, and minimizing breakage.
In The Event Of Broken Abstraction
Then there are events. The standard way a component interacts with its container, is through events. Events are just a fancy name for functions on an interface. We don’t want the component to know each and every possible container at compile time, so we give it an interface to shoot events. The containers implement that interface. That’s true also for the test container, and when the the component raises an event, the container “knows”. We can query it for what happened, or what was passed to it. We call it – testing the event.
It’s not abstraction at its best. We know what we need to find, and what to look for. Unless the component exposes the internal state, we rely on finding controls inside it. Any change in structure, identification or text may break. With common IDEs and languages, signature changes can be known, if not automatically refactored. So at least in that aspect, while abstraction is not complete (to say the least), it is possible to bypass some fragility issues between code and tests.
Now let’s go a-webbing.
UI DOM Component Testing
Before component frameworks, life was simple (not easy, not at all). We developed full web pages, and needed to test them whole, as they are. We still have the full-web automation tools, that automate browsers and pages. Selenium, Cypress, Playwright – All take the same ideas of locators and state for testing. Sort of.
Big set of pages are hard to develop, and test. So we have component testing frameworks, like React and Angular, and Vue.
With those frameworks, we also get testing tools, that act as containers for our components, like forms were for our desktop UI component controls. Angular even provides its own container, and calls it “test-bed”.
Angular test-bed, or testing libraries for all component libraries do that too. The container allows us to locate elements and manipulate them. Locators are used to find HTML elements. However, the state is not like a property. At least not as we defined properties on a class. State is defined as the content of HTML elements and attribute. In fact, nothing is passed in or out, like “regular” invocation. It’s just the DOM changes. Testing libraries help us check and assert these values.
Testing and responding to events is the same technically. The container gives a function handler to the component, and the component invokes based on the resulting HTML. Then we can check if the function was called, and with which arguments.
While it looks like the techniques are the same, the DOM case is more complex because of the extra transpilation. The model of state and flow is different. The resulting DOM must be known and accessible to the test. So not much of abstraction there.
And just to prove that, here’s a short story.
Abstraction of justice
Here’s an example of an abstraction problem I ran into. I created a React component that contained a drop-down list. I originally used React Select , and wrote a couple of tests for my parent component. It required finding the containing element of the text, to check if the right value was shown.
Then I decided to move to Material UI components. I innocently assumed that a drop-down remains a drop down, so the text will be in the same place I looked for it before. Nope. The containing element was different and needed to be queried differently.
Now, this was a novice assumption, but it comes from how I perceive components in the compiled world. What it does come down to, is our perception of abstraction, and its impact on testing.
The lesson is that when we’re talking about abstraction in desktop component testing, we use a common system (like a framework, e.g. .net forms, JavaFx) to take care of the abstraction. Final bits are controls, properties and events. In the case of DOM components, abstraction is almost completely off the table. There’s direct coupling between the resulting DOM and the test.
But we can create our own abstraction. That’s next time.