When test-driving a UI, should I test whether the text of the start button is “Start”? What about the CSS class or style of the button? It’s disabled state?

Of all the possible tests I could write, which are the ones that bring value? Which ones would get in my way later if I wrote them?

Description of the Kitchen Timer App

My name is David and I occasionally teach TDD trainings and also React trainings. In autumn 2019, I wrote a small React app in a test-driven way to practice and learn. In this video series, I want to show you some interesting things about developing a “Single Page Application” with React in a test-driven way.

The videos will not show you everything you need to develop your own app from scratch but only show you some aspects of doing TDD with React. You can find the code for the whole app on GitHub, and I will link it in the description.

The App itself is a simple “kitchen timer” that counts down from a user-defined time. It consists of only a few components, and I wrote all of them myself in a test-driven way, so, no libraries involved. The App component renders a Timer. And this Timer renders a Config panel and the timer view itself, which is an SVG that contains a Background component, the red Slice and a Foreground.

The app and source code of the components

In the last video, I showed you how you can test a React UI using a test runner and “enzyme”. Today, I want to talk about which aspects of the UI one might want to test and which aspects we better not test.

The decision to test or not test an aspect of the UI is subjective and will depend on your situation. But it will have consequences, both positive and negative.

Value and Cost, Tests that Get in Your Way

Generally speaking, every test we write provides some value, but also has a cost attached. We should only write tests that provide more value than they cost, and we should delete tests where the cost has now become larger than the value.

When we write tests after the production code, we must guess (“estimate”) value and cost. The value is that the test will hopefully catch a regression later, the cost is that the test might get in our way when we want to change functionality or even when we only want to refactor.

With TDD, there is another way the test provides value: It helps us think about the problem and it drives the implementation. So, when doing TDD, we know at least some of the value up-front. We know that we write tests that provide value.

But we should also only write tests where the expected or estimated cost is lower than the value. And this involves knowledge about how to write good tests, and also some educated guessing.

Today I want to talk about the “guessing”. For some ideas about how to write good tests, check out my other video serices “Red-Green”.

What could We Test? What Makes Sense?

When test-driving a React user interface with enzyme, there are many different aspects that I could test.

I can test whether a component contains another component.

it('renders the timer background', () => {
    const timer = shallow(<TestTimer/>);
    expect(timer.find(Background)).to.have.length(1);
});

I can test whether a component passes some data as a property or text to another component or a HTML element.

it('shows the remaining time, as passed in from the parent, when the timer is running', () => {
    const config = shallow(<Config remaining={{ mins: 8, secs: 5 }} status="running" />);

    expect(config.find('.remaining-mins').text()).to.equal('08');
    expect(config.find('.remaining-secs').text()).to.equal('05');
});

Source code of test that check values to display

I can ensure that the “Start” button is disabled when the timer is running and enabled when the timer is stopped.

it('renders the start button disabled when the timer is running', () => {
    const config = shallow(<Config status="running" />);

    expect(config.find('.start-timer').prop('disabled')).to.equal(true);
});
it('renders the start button not disabled when the timer is stopped', () => {
    const config = shallow(<Config status="stopped" />);

    expect(config.find('.start-timer').prop('disabled')).to.equal(false);
});

I can test whether an element has the correct class names or even which inline styles the element has.

it('tests a CSS class name, but does that make sense?', () => {
    const config = shallow(<Config status="stopped" />);

    expect(config.find('.start-timer').prop('className')).to.equal('start-timer');
});
it('tests a CSS style, but does that make sense?', () => {
    const config = shallow(<Config status="stopped" />);

    expect(config.find('.start-timer').props.style).to.have.property('opacity', 0.8);
});

I can test whether the text of the start button is indeed “Start”.

it('tests a button text, but does that make sense?', () => {
    const config = shallow(<Config status="stopped" />);

    expect(config.find('.start-timer').text()).to.equal('Start');
});

I can test how the component fetches the data it should display.

it('passes percentLeft to slice when timer is not running', () => {
    const timeToPercentage = () => 17.34;

    const TimerComp = createTimer(timeToPercentage);
    const timer = shallow(<TimerComp/>);
    expect(timer.find(Slice).prop('percentLeft')).to.equal(17.34);
});

And I can test what happens on a DOM event.

it('calls stopTimer when stop was pressed', () => {
    const stopTimer = sinon.stub();
    const config = shallow(<Config stopTimer={stopTimer} />);

    config.find('.stop-timer').simulate('click');

    sinon.assert.called(stopTimer);
});

Source code of test that checks what happens on a DOM event

Which of those tests make sense? To decide that, I must assess two risks:

  • This risk of the test getting in my way - Like, when I want to make a small, valid change and the test fails
  • And the risk of the application breaking - Without me noticing and without a test to catch it

In my case, the case of the timer, I only want to test-drive the timer behaviour. So, I would not write a test for the text of the start button or a CSS class or an inline style.

Those tests provide very little value while I am writing them and they also have little “regression-catching” value. Somebody changed the text of the start button? I bet that was on purpose and is not a regression.

Also, those tests come with a heavy costs. They will get in our way whenever somebody changes the visual design of the timer, even when the timer stays completely functional.

So, the tests that I would write are the ones that test timer functionality. Like, whether the start button is enabled and disabled correclty.

Or how the component fetches its data.

Whether it calls the right callback function when a DOM event did happen.

To Recap… Think about Value and Cost

Not all tests that we could write should be written. To decide whether I should test an aspect of the UI, I must think about

  • How much I need this test to drive the implementation
  • How likely this test will catch a regression later
  • How likely this test will get in my way later

The decision involves some educated guessing and requires some experience. But don’t worry if you get it wrong - Just delete the tests that have out-lived their usefulness.

Let’s Have a Discussion!

How do you decide which tests to write and what to leave un-tested? Let’s have a discussion - Please write a comment on YouTube!

Do you want to learn and practice TDD, maybe even together with React? Then check out our training offers at devteams.at/services/training.

And follow me on Twitter - I am @dtanzer there - so you’ll not miss the next video where I will talk about managing dependencies of react components and dependency injection.

Read / watch all parts of “React TDD” here: