Unit testing principle, while applied to UI, can be quite confusing. I’d like to open a discussion and see why that is the case.
Say we have a UI component:
import { useState } from 'react'
const App = () => {
const [value, setValue] = useState('')
const onChange = e => { setValue(e.target.value) }
return <input value={value} onChange={onChange} />
}
The above is one of the most atomic thing happened in the UI. The unit here is apparently the App
component where it prints an input that can be used to collect user inputs.
Say we want to test it as in the classical unit testing way. What would you do? Mock the useState
, since this is the only dependency, and App
has no input. Now we can compare the rendered DOM with our expectation and see whether we are getting the right HTML.
Correct as the above unit test is, I would say this is the most pathetic thing I ever see as an engineering. Because literally none of what I mentioned needs to be tested, even a monkey can tell you that. You might argue this is what textbook want us to do. That might be true. But the textbook was written before the “Frontend Engineer” title was created. Who cares about the textbook if it doesn’t smell right.
Let’s rethink this from a different angle.
Who’s the audience of a unit?
The reason I see the above classical unit test pointless is, more or less from my experience as an UI engineer. Back twenty years ago, all we were doing is algorithm. So the audience of a unit is always a coder. Therefore the inputs are function’s input parameters naturally. Same as the output which is what the function outputs.
But now let’s switch to a UI component. What is the audience of the component? Yes, it can be a developer who’s going to consume your component; but wait a second, should a customer or user be your main audience? If we take the user as the audience, then the input should include user’s input device, shouldn’t it? Also the output should include the change happened due to the user’s input. Our job here is to let the customer test this unit.
Let’s look at our code again:
import { useState } from 'react'
const App = () => {
const [value, setValue] = useState('')
const onChange = e => { setValue(e.target.value) }
return <input value={value} onChange={onChange} />
}
If we do things from the customer’s perspective, what we should test is the change on input
caused by the event fired to onChange
. Ok, making sense. But what about useState
? Let’s leave it there, mount (or render) the App
under the React environment in an isolated scope in a browser. And after that I’ll check what happened to the screen. Isn’t this natural for the UI component? Or you prefer the classical unit test?
People would argue basically I did an end-to-end testing, because I used Cypress
or Playwright
to get my job done. If we really have to stick with these textbook definition, then I’ll say “Unit testing a UI component is pointless”. But what I really want to say is, the unit in UI component unit tests should go wider to include the user.
Adding the user as the input of a unit shouldn’t break the rule or definition of a unit test. A unit needs to be used! Not just rendered.
Moreover adding the user as the input of a unit shouldn’t push us right into the end-to-end testing category, which is a lot more broader than the unit testing.
Summary
Watch out for the definition of your unit when you write unit tests. Because you don’t want to waste time pretending you are writing tests. Watch out for your tests’ audience especially if you really care about the robustness of your unit. Fuck the classical definition if you don’t think it fits your needs. Terminologies come next after things are wired right.