A rambling test-first example

adnauseum

Samuel Kendrick

Posted on April 26, 2023

A rambling test-first example

You've been assigned at ticket! Check it out: https://github.com/VEuPathDB/web-eda/issues/1596

Here's a picture of the mocks:

Image description

I've already built out the map and the componentry outlined in pink. The scope of this work is to implement the blue.

Image description

People like to say they have good attention to detail. Those folks don't need test-first development (aka TDD). For other mortals, such as myself, I like to have a schematic and then implement code based on that schematic. You know who else does that? Civil engineers. Mechanical engineers. Chemical engineers. Electrical engineers. Basically everyone but software engineers.

My first schematic focused on converting the design into end-user behaviors:

import { render, screen } from '@testing-library/react';
import { MarkerConfigurationMenu } from './MarkerConfigurationMenu';

describe('<MarkerConfigurationMenu />', () => {
  test('users can select a marker type', async () => {  });
  test('when a user selects a marker type, a marker-specific configuration panel appears', async () => {  });
  test('users can configure their marker selection', async () => {  });
});
Enter fullscreen mode Exit fullscreen mode

I also have another user in mind: coworkers. Put another way, my component should have other behaviors which the end-user doesn't care about. For example, the developers using this component will consume it's API (via props). One such behavior would be to fire an event for each time the user updates the marker configuration. As it is, many other components will care about what the user has selected. For instance, this legend needs to know what the user has selected.

Image description

Fleshing out the tests

import { render, screen } from '@testing-library/react';
import { MarkerConfigurationMenu } from './MarkerConfigurationMenu';
import { useState } from 'react';

const markerTypes = [
  {
    name: 'Donuts',
    renderConfigurationMenu: function DonutConfigurationMenu() {
      const [frostingSelection, setFrostingSelection] = useState('');
      return (
        <div>
          {frostingSelection.length > 0 ? (
            <p>A donut with {frostingSelection} frosting.</p>
          ) : (
            <></>
          )}
          <label htmlFor="frostingFlavor">
            Frosting flavor
            <select
              onChange={(event) => setFrostingSelection(event.target.value)}
              name="frostingFlavor"
              id="frostingFlavor"
            >
              <option value="">Unfrosted</option>
              <option value="strawberry">Strawberry</option>
              <option value="chocolate">Chocolate</option>
            </select>
          </label>
        </div>
      );
    },
  },
  {
    name: 'Bar plots',
    renderConfigurationMenu: function BarPlotConfigurationMenu() {
      const [barSelection, setBarSelection] = useState('Cheers');
      const [plotSelection, setPlotSelection] = useState('Novel');

      function handleSelection(event: React.ChangeEvent<HTMLSelectElement>) {
        const { name, value } = event.target;
        name === 'bar' ? setBarSelection(value) : setPlotSelection(value);
      }

      return (
        <div>
          <p>
            You will be plotting a {plotSelection} at {barSelection}.
          </p>

          <label htmlFor="bar">
            Bar options
            <select
              onChange={handleSelection}
              value={barSelection}
              name="bar"
              id="bar"
            >
              <option value="Cheers">Cheers (Bostom, MA)</option>
              <option value="The Blue Bar">The Blue Bar (NYC)</option>
              <option value="Harry's Bar">Harry's Bar (Venice, IT)</option>
            </select>
          </label>

          <label htmlFor="plot">
            Plot options
            <select
              onChange={handleSelection}
              value={plotSelection}
              name="plot"
              id="plot"
            >
              <option value="Novel">Novel</option>
              <option value="Bank Heist">Bank Heist</option>
              <option value="Points">Points on a graph</option>
            </select>
          </label>
        </div>
      );
    },
  },
];

describe('<MarkerConfigurationMenu />', () => {
  test('after a user selects a marker type, a marker-specific configuration panel appears', async () => {
    render(<MarkerConfigurationMenu markerTypes={markerTypes} />);

    screen.getByText('Donuts').click();
    screen.getByText('Frosting flavor');
    screen.getByText('Strawberry').click();

    expect(screen.getByText('A donut with strawberry frosting')).toBeVisible();

    screen.getByText('Bar plots').click();

    expect(
      screen.getByText('You will be plotting a novel at Cheers.')
    ).toBeVisible();
  });

  test('users can configure their marker selection', async () => {
    render(<MarkerConfigurationMenu markerTypes={markerTypes} />);

    screen.getByText('Bar plots').click();
    screen.getByLabelText('Bar options').click();
    screen.getByLabelText('The Blue Bar').click();

    expect(
      screen.getByText('You will be plotting a novel at The Blue Bar.')
    ).toBeVisible();
  });
});

Enter fullscreen mode Exit fullscreen mode

I think it'd be wise for the <MarkerConfigurationMenu /> to have one responsibility: rendering the selected marker configuration. The UI for each marker configuration is drastically different for each given marker type. Looky here:

Bar plots:

Image description

Donuts:

Image description

<MarkerConfigurationMenu /> doesn't actually care about the particulars of the marker configuration.

Actually, I'm going to rename things:

import { render, screen } from '@testing-library/react';
import { MarkerConfigurationSelector } from './MarkerConfigurationSelector';
import { useState } from 'react';

const markerTypes = [
  {
    name: 'Donuts',
    icon: () => {},
    renderConfigurationMenu: function DonutConfigurationMenu() {
      const [frostingSelection, setFrostingSelection] = useState('');
      return (
        <div>
          {frostingSelection.length > 0 ? (
            <p>A donut with {frostingSelection} frosting.</p>
          ) : (
            <></>
          )}
          <label htmlFor="frostingFlavor">
            Frosting flavor
            <select
              onChange={(event) => setFrostingSelection(event.target.value)}
              name="frostingFlavor"
              id="frostingFlavor"
            >
              <option value="">Unfrosted</option>
              <option value="strawberry">Strawberry</option>
              <option value="chocolate">Chocolate</option>
            </select>
          </label>
        </div>
      );
    },
  },
  {
    name: 'Bar plots',
    icon: () => {},
    renderConfigurationMenu: function BarPlotConfigurationMenu() {
      const [barSelection, setBarSelection] = useState('Cheers');
      const [plotSelection, setPlotSelection] = useState('Novel');

      function handleSelection(event: React.ChangeEvent<HTMLSelectElement>) {
        const { name, value } = event.target;
        name === 'bar' ? setBarSelection(value) : setPlotSelection(value);
      }

      return (
        <div>
          <p>
            You will be plotting a {plotSelection} at {barSelection}.
          </p>

          <label htmlFor="bar">
            Bar options
            <select
              onChange={handleSelection}
              value={barSelection}
              name="bar"
              id="bar"
            >
              <option value="Cheers">Cheers (Bostom, MA)</option>
              <option value="The Blue Bar">The Blue Bar (NYC)</option>
              <option value="Harry's Bar">Harry's Bar (Venice, IT)</option>
            </select>
          </label>

          <label htmlFor="plot">
            Plot options
            <select
              onChange={handleSelection}
              value={plotSelection}
              name="plot"
              id="plot"
            >
              <option value="Novel">Novel</option>
              <option value="Bank Heist">Bank Heist</option>
              <option value="Points">Points on a graph</option>
            </select>
          </label>
        </div>
      );
    },
  },
];

describe('<MarkerConfigurationMenu />', () => {
  test('after a user selects a marker type, a marker-specific configuration panel appears', async () => {
    render(<MarkerConfigurationSelector markerTypes={markerTypes} />);

    screen.getByText('Donuts').click();
    screen.getByText('Frosting flavor');
    screen.getByText('Strawberry').click();

    expect(screen.getByText('A donut with strawberry frosting')).toBeVisible();

    screen.getByText('Bar plots').click();

    expect(
      screen.getByText('You will be plotting a novel at Cheers.')
    ).toBeVisible();
  });

  test('users can configure their marker selection', async () => {
    render(<MarkerConfigurationSelector markerTypes={markerTypes} />);

    screen.getByText('Bar plots').click();
    screen.getByLabelText('Bar options').click();
    screen.getByLabelText('The Blue Bar').click();

    expect(
      screen.getByText('You will be plotting a novel at The Blue Bar.')
    ).toBeVisible();
  });
});
Enter fullscreen mode Exit fullscreen mode

Now at this point I have the privilege of showing the specs to my coworkers and get their feedback!

💖 💪 🙅 🚩
adnauseum
Samuel Kendrick

Posted on April 26, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

A rambling test-first example
testing A rambling test-first example

April 26, 2023