A refreshing way to test the frontend

Bruno Noriller - Jul 17 '22 - - Dev Community

I gave it an earnest try!

All of that was in Better Specs and Better Tests, as much as I could, especially the parts I had my doubts about.

Guess what? I liked it!

The project

Just so we are on the same page, here’s the deployed version so you can just see what it does:

https://refreshing-way-test.vercel.app/

And the Github repo:

https://github.com/Noriller/refreshing-way-test

How it should work

Basically, it’s just a form with two inputs and a button.

Fill in the inputs, click the button and you get the ID of the created resource.

(I’m using jsonplaceholder API, so nothing is actually created)

And if you don’t fill something, it shows you errors.

How it was done

I’ve used Vite to create a React project and took the chance to try out Vitest for testing, I’m also using Testing Library.

Inside the test files, it's no different than Jest, so no problem there.

The setup was easy enough but I also didn't need to do any special configuration.

The running of the tests is fast!

And they also have a VSCODE extension that makes it easy to run and debug them.

I use Wallaby, which is paid and totally worth it, but I'm really impressed and already recommending you to use their extension if your project is using Vitest.


The testing

Now that we’re on the same page, the testing.

The two biggest things I have changed, from my previous approach, were to use the “single expectation” tests, this also lead me to use a lot more nesting with describe blocks where I could use two of the A’s of testing (arrange, act) and then let the final one for the it/test blocks (assert).

I’ve also stopped using “should” and end up with the “describing the expected behavior”.

The result

The result is this test file:

https://github.com/Noriller/refreshing-way-test/blob/master/src/app.spec.jsx

On the describe blocks I either arrange or act then on the it I assert.

I’m using the beforeEach to either render or do something and if you use ESLINT with the recommended rules for Testing Library you should probably see some error if you try that.

I understand the reasons behind this, but even then, with the current API of Testing Library, you don’t really need to initialize anything since you can do everything using screen.

What I do agree with is that in text format you might be lost as to what is being done and at which point. But on a code editor where you can just collapse things and easily navigate, this shouldn’t be a problem.


But in any case, you can still do something like this:

https://github.com/Noriller/refreshing-way-test/blob/master/src/app.version2.spec.jsx

This way you know exactly what’s going on on each test, at the cost of having to copy the steps everywhere.

In this example, I hoisted everything I would need and gave them names easy to understand, but when they didn’t fit or it was just a “one-off”, then I just used what I needed.


So… which one did you like more or which one do you use or got interested enough to try?


The console

As you run the tests (check the README), you will see something like this:

✓ src/app.version2.spec.jsx (27)
    ✓ <App> (27)
        ✓ on default render (27)
            ✓ renders text of not submitted
            ✓ renders input for title
            ✓ renders input for body
            ✓ renders a button (2)
                ✓ with submit text
                ✓ that is enabled
            ✓ dont render the title error label
            ✓ dont render the body error label
            ✓ when you submit a form (20)
                ✓ inputting both values (9)
                    ✓ the title input has the input value
                    ✓ the body input has the input value
                    ✓ when submitting (7)
                        ✓ disables the button
                        ✓ after api call complete (6)
                            ✓ reenables the button
                            ✓ renders the id
                            ✓ has called the API once
                            ✓ has called the API with
                            ✓ changes the text with the id
                            ✓ clears the form
                ✓ without inputting values (3)
                    ✓ shows a title error
                    ✓ shows a body error
                    ✓ doesnt call the API
                ✓ inputting only the title (4)
                    ✓ dont show a title error
                    ✓ shows a body error
                    ✓ doesnt call the API
                    ✓ dont clear the form
                ✓ inputting only the body (4)
                    ✓ shows a title error
                    ✓ dont show a body error
                    ✓ doesnt call the API
                    ✓ dont clear the form
Enter fullscreen mode Exit fullscreen mode

Or, you can end up with something like this:

- <App> on default render renders text of not submitted
- <App> on default render renders input for title
- <App> on default render renders input for body
- <App> on default render renders a button with submit text
- <App> on default render renders a button that is enabled
- <App> on default render dont render the title error label
- <App> on default render dont render the body error label
- <App> on default render when you submit a form inputting both values the title input has the input value
- <App> on default render when you submit a form inputting both values the body input has the input value
- <App> on default render when you submit a form inputting both values when submitting disables the button
- <App> on default render when you submit a form inputting both values when submitting after api call complete reenables the button
- <App> on default render when you submit a form inputting both values when submitting after api call complete renders the id
- <App> on default render when you submit a form inputting both values when submitting after api call complete has called the API once
- <App> on default render when you submit a form inputting both values when submitting after api call complete has called the API with
- <App> on default render when you submit a form inputting both values when submitting after api call complete changes the text with the id
- <App> on default render when you submit a form inputting both values when submitting after api call complete clears the form
- <App> on default render when you submit a form without inputting values shows a title error
- <App> on default render when you submit a form without inputting values shows a body error
- <App> on default render when you submit a form without inputting values doesnt call the API
- <App> on default render when you submit a form inputting only the title dont show a title error
- <App> on default render when you submit a form inputting only the title shows a body error
- <App> on default render when you submit a form inputting only the title doesnt call the API
- <App> on default render when you submit a form inputting only the title dont clear the form
- <App> on default render when you submit a form inputting only the body shows a title error
- <App> on default render when you submit a form inputting only the body dont show a body error
- <App> on default render when you submit a form inputting only the body doesnt call the API
- <App> on default render when you submit a form inputting only the body dont clear the form
Enter fullscreen mode Exit fullscreen mode

Which is not unlike what you would get in case of an error.

FAIL src/app.version2.spec.jsx > <App> > on default render > when you submit a form > inputting both values > when submitting > after api call complete > clears the form

As much as I want to have tests saying what they are doing, I hardly manage to make something this specific.

But that is something that was just a happy accident, it simply happen and I was as surprised as you.


Pros and cons

Pros

Since you split the arrange and act into blocks, I feel it makes it easier to catch cases, because at each new nested block you can focus on the current block and see all the "what if's" that you can do.

More than that, it lets you think on a smaller step each time, I feel like I don't need to think about the entire behavior of a block, just on the individual one I'm on. This atomicity also helps with TDD.

This also makes it possible to use something like BDD to write specifications on the "user journey" for each part of the application.

Cons

Verbosity is a given with this approach. I'm not even talking about the two different versions, but more about that you explode the assertion blocks that would normally live in one test block to multiple ones.

Another one would probably be performance. Something that you would do one time in one test, now is done over and over again in multiple.


Are you refreshed?

This is a different way of testing, this even changed the way I’ve approached some tests I’ve made.

While this can be used on the backend (and I’m using it), on the frontend I feel like it’s TDD.

I’ve tried TDD on the frontend before, but that hasn't gone well. But with this approach, after the code is done I can still think back step by step of what is going on, find edge cases and fill in the others.

Considering the test is usually done, this doesn’t really fit the norm.

https://github.com/Noriller/refreshing-way-test/blob/master/src/app.version3.spec.jsx

✓ src/app.version3.spec.jsx (7)
   ✓ <App> (7)
     ✓ on default render (7)
       ✓ renders the component
       ✓ when you submit a form (6)
         ✓ inputting both values (3)
           ✓ has both values
           ✓ when submitting (2)
             ✓ disables the button
             ✓ after api call complete (1)
               ✓ get the id and clears the form
         ✓ without inputting values (1)
           ✓ shows errors
         ✓ inputting only the title (1)
           ✓ shows error for the body
         ✓ inputting only the body (1)
           ✓ shows error for the title
Enter fullscreen mode Exit fullscreen mode

I’ve just refactored the third version. I hate it.

It’s smaller and runs faster, yes. But I couldn’t express what the tests actually do. I have to use some generic wording or just omit a lot of stuff that is happening.

Not only that, I know I did even worse than that because in this one I at least kept the describe blocks so there’s at least some separation, but I know I would usually have even less than that.

Tests are also code and code should be clean and legible.

For the third one you would be inclined to maybe add a lot of comments, or just let it be as is.

Meanwhile the first two you could maybe call “self-documenting” code.


With this, I just urge you to try it.

Try it and then come back and say what you think, better yet… leave a comment for yourself here! Say what you think of it now and then come back to check if it continues or if you’re going to change something.


Cover Photo by National Cancer Institute on Unsplash

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .