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
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
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
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