Re-writing Rabbit Hole

Wherein I begin unit testing some components I wrote in Vue.

So…I looked too closely at the rash of Vue widgets I wrote a while back and hosted on WidgetWonk.com. I never intended those widgets to be anything like production grade. They were more just practice for me to learn various techniques I didn’t already know very well. But now that I’ve started looking again at them, oy! I’m sure I meant to write some unit testing for these Vue components eventually. But that fact is I never really learned to write unit tests for interactive, front-facing things like a web GUI before. It’s something I only started to think about seriously quite recently when I was studying how to use dom-testing-library (https://testing-library.com).

I will be reviewing all of my Vue widgets, starting with the little shopping list app I created, Shop. The code for that one is at https://github.com/xerocross/xerocross.shop. My priorities for now are (1) to make sure my widgets still work with the current version of Vue—which will require some patching here and there—and (2) adding unit tests for my components. I will be using the @vue/test-utils package along with Jest as my test runner. Also, the style of test I write will be heavily influence by dom-testing-library. Vue’s test-utils library does not seem to enforce that kind of testing style/strategy on us, but I liked it. I like the idea of tests that interact with the code as much like a user interaction as possible, especially when talking about something like a front-facing user interface device.

On that note: I would use a different strategy to test a utility library. In that situation, I want to make sure every damn function is functioning properly. That is: I don’t like to only test the interface of a library. I want to know that all the internals are working properly also. That’s not always easy to accomplish, but for now it’s not our topic. But it does affect our current considerations as follows: if anything seems tricky enough that it deserves unit testing beyond what you can accomplish just by mimicking user interface interactions, maybe that is something that should be factored out into a helper module and tested with different techniques.

Just to make all this more explicit, let me show you a picture of a small component I wrote.

Number input component.

Its only purpose is to let users enter a simple number, like a price, using either button clicks or (more likely) by tapping on a phone screen. There was no earthy reason for me to build this component other than practice. I don’t have to check first to know that somebody out there has already made a number input component that puts mine to shame.

The whole and entire purpose of this thing is user interactivity. If you want to test that it works, it does not make sense to directly poke at the internals, at the data model. You should poke at the buttons, just like a user would, and then read the number display just like a user would. The @vue/test-utils package lets us do that, but I did have to make some small changes to the component to help with testing.

This testing library allows you to mount a component. First you just import { mount } from '@vue/test-utils' at the top of the page and import the component, say import NumEnter from "./num-enter.vue". Then for testing you can mount by executing const wrapper = mount(NumEnter). They cover this part fairly well in their docs. Check out this page for more details worth reading: https://vue-test-utils.vuejs.org/api/wrapper/#properties.

The thing I want to emphasize is that you can use vue’s test-utils to implement DOM-centric testing. I don’t claim to be an authority on the topic exactly yet, but I can tell you something that worked for me. To test this particular widget, I needed a way to search for the particular buttons—to query for them using standard selector syntax. I have 12 buttons that each need to be identified uniquely by their rendered HTML. And don’t start thinking about using HTML ID attributes, because for obvious reasons it is illegal for two elements to have the same ID and you should always write a component assuming there might be many of them on a single page. In this particular situation where I have buttons like “the 1 button” and “the dot/decimal button” I decided to simply give each button a unique class name of the form “btn-1”, “btn-2”, “btn-c”, and the like. With that done, inside unit tests we can grab individual buttons by doing something like this.

const wrapper = mount(NumEnter);
const button1 = wrapper.find(".btn-1");

I found it useful to even abstract away some of the details as follows. For each test, after mounting a new wrapper I get the buttons using this function.

function getButtons (wrapper) {
    let buttons = {};
    for (let i = 0; i < 10; i++) {
        buttons[i] =  wrapper.find(".btn-" + i);
    }
    buttons["c"] = wrapper.find(".btn-c");
    buttons["dec"] = wrapper.find(".btn-dec");
    buttons.click = function(val) {
        buttons[val].trigger("click");
    }
    return buttons;
}

Then I can fire click events by executing code like buttons.click("5"). It doesn’t just save time. It makes the code more readable and understandable.

Now, the thing I need to test to see if the button clicks are actually working—this is a place I make a mistake at first. Using this testing library, it is possible to directly access the internal data where I store the string that gets displayed, the “52.05” or whatever number the user enters. For practical purposes, I stored the number internally as a string and put safeguards in place to validate input. You can access that number string in my case by referencing wrapper.vm.numberString. So when I first started writing these tests I thought, ok, then we just check the value of that string and make sure it gets updated correctly after the various button clicks.

But that doesn’t keep with the style of testing we are doing. We don’t want to test internal data. We want to test what a user sees. This is an interface. So everyplace where I had written something like expect(wrapper.vm.numberString).toBe("2") I replaced it with code that reads the content that is actually displayed to the user. We fully expect those two things to agree, to be the same, but it matters which one you check. This is a unit test. We write it because we expect it to succeed. To do this, I also have to attach a new identifier class to the input element there, “num-enter-view “. Then, to view its contents, I can refer to wrapper.find(".num-enter-view").element.value. Now, assuming the library works as promised, we have the actual value of that DOM element. That’s as close as we can get to verifying what the user sees on the screen. After that it is up to the browser.

This seems to work as expected. After figuring out the basic structure of writing tests using this library and in this style, I now have a small battery of tests that covers the use cases I can imagine. And it works. We don’t seem to have any weird race condition problems or the like. Questions remain. Just like in other frameworks, these events like button clicks do not tend to fire off their handlers synchronously. Instead, what typically happens is it queues an update to occur whenever that’s convenient for the framework and the browser. It’s asynchronous, but it happens so quickly you don’t know the difference. And the result appears faster and better than if it had been synchronous.

But when you write a function with one line of code after another, the interpreter just executes those lines one after another, synchronously, unless you specify otherwise. That is the default behavior of JavaScript. Because my tests are working without me having to explicitly deal with this a-synchronicity, I must assume that the testing library handles it. When you execute something like the built-in [button].trigger(“click”), the library must be taking care of the asynchronous nature of event handling and somehow flattening it for us so that by the time the next line in our test is executed the previous line and any asynchronous activity triggered as a result of it has finished. I can see how that might be done, but I prefer not to speculate aloud right now, and I also don’t want to dive into research mode. I just want us all to be aware that somebody wrote some excellent code for us to use.

Later I might be—probably will be—in the mood to dive into the research and see what’s actually going on.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s