DEV Community

Ahmed Al-Obaidi
Ahmed Al-Obaidi

Posted on

Lessons learned in modern UI testing

I've been developing modern React-based UIs for a few months now, while also maintaining not-so-modern ones, and I recently faced a problem: I've tested web services before, but on the front-end, I don't even know what to test; the problem wasn't that I couldn't find a tool to test with, there are many interesting testing libraries out there, the problem was that I didn't even know how to approach testing UIs to begin with; our front-end tech lead is nice enough to let us try things around, so I did, and it was .. well .. frustrating, I noticed that most of the tests that i was writing were either breaking easily or were downright useless, so I read articles here and there, and did Kent C. Dodds testing courses titled “JavaScript testing practices and principles” and “Testing React applications” that are available on Frontendmasters; below is a summary of what I think are the most important points that were mentioned:

  • Tests should not force you, or even encourage you, to rewrite parts of your implementation.

  • Code coverage is almost always a bad metric for how confident you are in the code you’re shipping; it’s also incorrectly used as a metric for code quality.

  • Code coverage tools implicitly assume that all blocks, statements, and lines of code are equal, but in reality, some are a lot more critical than others.

  • The last 10% of code coverage is usually covered by tests that are hard to maintain.

  • Tests should explicitly communicate the relationships between the expected result and other entities that are directly causing this result.

  • Tests that resemble how the entity is actually used give more confidence in that entity.

  • Always be explicit about your mocks.

  • When mocking, you must ensure that the mocked entity and the entity that you’re testing are actually connected as expected, for example if you're calling an API and you mock this call, then you must first ensure that the entity you’re testing is actually able to send requests to that endpoint, you don’t have to write a test to check this kind of connection, a manual check is enough.

  • In the formal definition of a unit test you mock every single external dependency, otherwise you're writing an integration test; when you mock everything you're focusing on this very specific entity that you're testing, it's fine to have shallow mocks as long as they behave as your entity expects them to behave.

  • Clean up your testing DB before each test instead of after, this ensures that if a test fails, your DB would be in a debug-able state.

  • testing the CSS, or the overall styling of an entity is usually best done using visual regression tools, but if you're doing CSS-in-JS then you can do snapshot testing that includes your CSS; this is considered a middle ground between regular tests and visual regression tests.

  • the problem with Enzyme is that it encourages testing implementation details, it causes extreme cases of white-box testing, for example: shallow rendering implicitly mocks all inner components even if they live within the same module/file, another example would be the “instance()” method, which gives you access to a component's class instance so that you can call methods directly, which encourages breaking the connection between the actual component and it’s methods.

  • Snapshots can include the props of your components; however, including them is considering an implementation detail, because users don’t care about props.

  • Good tests are the ones that would guide you through a refactoring process, tests that just fail when you do any kind of refactoring are not useful.

  • Do your best not to test state changes, users don’t care about how your internal state is changing, testing how these changes affect your UI is a lot more useful.

  • Snapshots can be easily abused; if someone runs a snapshot test, and it fails, and they don't carefully read the log before updating the snapshot, then this test is not even a test anymore; Snapshots heavily depend on the commitment of the team to be intentional about maintaining their tests.

  • Snapshots tend to have a higher signal-to-noise ratio, it's difficult for someone to tell which parts are important, and which aren't, that’s why we should keep our snapshots small.

  • Snapshots are useful not just for DOM and DOM-like assertions, but also for object equality assertions, and specific error throwing assertion; they are easier to maintain compared to manually writing down the equality check, which is the more common approach for those types of assertions.

  • If a component’s smoke test fails because it expects a provider ( Redux store, browserRouter, etc..) then you should simply render it with that provider.

  • It’s perfectly fine to first setup the test by doing manipulations that depend on implementation details in order to reflect a specific scenario that your test depends on, just make sure that the actual core of your test is not following the same approach.

I'm interested to see how things will go from here.
At the end I'd like to share this treasure with you.

Top comments (1)

Collapse
 
lenaggar profile image
Mahmoud ElNaggar

"Good tests are the ones that would guide you through a refactoring process, tests that just fail when you do any kind of refactoring are not useful."

amen to that