Skip to main content Daniela Valero

Don't test implementation details: What does it mean for react testing?

Developer conding

Illustration from ls.graphics

Glossary: Setting the ground for the article #

Unit Tests: Testing of individual units like functions or classes by supplying input and making sure the output is as expected.

Testing structure refers to the organization of your tests. Nowadays, tests are usually organized in a BDD structure that supports behavior-driven development (BDD).

behavior-driven development (BDD) is a branch of Test Driven Development (TDD). BDD uses human-readable descriptions of software user requirements as the basis for software tests.

    describe('calculator', function() {
// describes a module with nested "describe" functions
describe('add', function() {
// specify the expected behavior
it('should add 2 numbers', function() {
//Use assertion functions to test the expected behavior
...
})
})

Note that the general idea behind BDD, is the same that users of cucumber have. (aka: UI automation)

Functional Tests Functional tests are written from the user’s perspective and focus on system behavior that users are interested in. For the scope of this article, I will refer to this type of tests like the testing of individual units of code (such as functions or modules) in isolation from the rest of the application (meaning: not E2E testing)

Assertion functions are used to make sure that tested variables contain the expected value. Snapshot Testing is when you compare a data structure to an expected one.

What is the difference between unit and functional tests? I like the classification of “unit tests” for developer-perspective code units, and “functional tests” for user-perspective UI tests. - Erick Elliot

In other words: Unit tests: The dev. writes it Functional test: The QA engineer writes it

Source of all these definitions

Part 1: Unit testing mindset. #

Why people advocate for testing behaviour rather than implementation details?

“Either way, it’s generally a good idea to treat your tests as black box tests, meaning that the test code should not care about the implementation details of the feature you’re testing” Erick Elliot

When we write a test for a component, thinking that the component is a black box, then it is natural to go with the behavior approach. In pragmatic terms this translates to:

  1. Render a react component with the required properties, so that it renders as we need it.
  2. Trigger a mock of a user interaction
  3. Assert that what we need to happen actually happens

In contrast, writing a test that relies on implementation details, we can also call this white box, the steps are longer

  1. Render a react component with the required properties, so that it renders as we need it.
  2. Access to the state of the component and update it accordingly, or access to a local function and re-build manually all the params it needs
  3. Trigger a mock of a user interaction or call directly the local function
  4. Assert that what we need to happen actually happens

Because we need to build manually in our test the required state and params that our function needs as input, we are actually relying on implementation details.

The problem with this:

  1. We write in our test all the required input that our component or function needs, in the perfect scenario, so that the test passes. As we are the developers writing that code, we are blind towards the scenarios that could happen in the real world, so our tests will not be necessarily realistic. Therefore, it gives you a false sense of security over your unit tests.
  2. Software is in constant change, requirements and features are always moving, specially within the product engineering mindset. What we build, is supposed to live long, and as it does, the implementation of our code will change. If we rely too tightly to implementation details in our tests, then we will have double work when we need to update that code. If we don’t rely on implementation details, when we change the code we will not necessarily have the need to update our tests.

Part 2: Putting things into mental boxes #

When react people speak about react-testing-library, and not testing implementation details, we are not talking about Functional Tests. The reason for this is that when we are writing a test in our development process, we are testing a small, single unit of functionality within the scope of a feature development.

So, the terms can confuse us to think: If I will write a test from the user point of view for my component, then why should I write it at all if the QA engineer will write a very similar one?

Well, the QA engineer is writing their test having a broader view, they will test bigger pieces of functionality, rather than the click of a button in a form. These tests basically ensure that the system works as a whole.

In short: Functional tests help us build the right product. (Validation) Unit tests help us build the product right. (Verification) Source

Part 2.1: How to start writing a test? #

You can start asking yourself these questions that Eric Elliot suggests

  • What component aspect are you testing?
  • What should the feature do? What specific behavior requirement are you testing?

Part 2.2: How to write or structure a specific test #

Whatever you do, ensure that if your test fails, it tells you in a glance all the following information. This way you or the reader will know directly what has happened.

  • What is the unit under test (module, function, class, component, etc)?
  • What should it do? (Prose description) What was the actual output?
  • What was the expected output? How do you reproduce the failure?

These are webmentions via the IndieWeb and webmention.io.