Testing Tests My Patience — Part 2

Jamesfeltonthomas
8 min readApr 1, 2024

--

Bad unit test meme

Recap

We organized our directories, named our files along with the testing suites, and finally handled the Happy Path test all in part 1 of this blog series. If you haven’t read it hop back and catch up with us by clicking here. Now that you’re caught up let’s move forward by breaking down what exactly software testing is and where we fall in that definition.

Just What Kind of Testing Are You Doing?

Up to this point in my coding career, I have used the term “Software Testing” interchangeably with any task that required me to systematically ensure that the code structures I created worked as intended. That made sense to me, but writing this blog series opened my eyes to how far off I was from the truth. So let’s be exact about what software testing encompasses.

Software Testing is a systematic process to identify the correctness of functionality an application possesses by verifying and then validating that it is error-free. Proper testing considers all aspects of an application ( Reliability, Scalability, Portability, Usability, and Re-usability ) to ensure the software’s fitness to meet specified or client requirements. Once you look at the diagram depicting the levels of Software Testing, you will see the term isn’t interchangeable.

This a visual diagram of the hierarchy of software testing

If you carefully searched the picture you’re now wondering at what level component testing falls because I know I was. Following additional research I discovered that component testing falls into the black-box, while unit testing is in the white-box category, but that doesn’t shed any light on their differences for me.

Component vs. Unit testing examples:

  • Unit tests are conducted on small individual parts of the code (a single method ), while component tests are larger and conducted on related classes, modules, or functions ( a component ).
  • Component tests are executed at the application level, while unit tests are executed at a granular level.
  • In component testing, the test engineer does “NOT” become aware of the internal planning of the software ( black-box testing ), in contrast to unit testing where the developer is aware of the internal planning of the software ( grey-box testing ).
  • Component testing is performed by validating use cases and test requirements (“When a user comes to this page, they should be able to …” ), while Unit testing assesses the application conformity to the design documents ( “pressing the open button will render a dialog on the screen” ).

The topic of software testing is far too vast to be covered in a single section or even a blog so I’m going to let you dive into that water on your own. I left a few things to get you started at the end of the article.

The Sad Path

We want to provide the best user experience regardless of good or bad times so we test both the Happy and Sad paths. Here is my simple explanation; sad is the opposite of happy, and when you’re sad something usually has gone awry so “Sad Path” testing assesses a component’s reaction to unexpected events. The user will undoubtedly interact with the application in new and challenging ways, so sad path testing is performed, ensuring that our component handles errors gracefully.

The Sad Path will regularly involve testing responses to incorrect login values, improper function arguments, invalid data formats or syntaxes, and even system failures. Since our component was extremely basic our first test was for proper rendering to the DOM, so our sad path will be the opposite, testing for structures that should NOT have rendered. I hope I didn’t lose you on our trek through the levels of software testing because we just got to the good stuff; here comes the code.

The Code

To showcase our Sad Path test, I’m using a new component from the elevator app. The Home component used as an example in part 1 of this series had no external data inputs, or conditional renders, so testing for things that would never be rendered would be redundant. I chose the component ButtonDialog for our example suite because it allows us to test for code structures rendered upon mount and those responding to user interactions. If you’re interested, you can browse through the entire elevator application here, and apply the information from this blog to build another testing suite to practice the AAA format. If you need a refresher on AAA you find a great resource here.

ButtonDialog.js

const ButtonDialog = ({ data }) => {
const { Title, Desc, List } = data;
const [openDialog, setOpenDialog] = useState(false);

const handleClickClose = () => {
setOpenDialog(false);
};

const handleClickOpen = () => {
setOpenDialog(true);
};

return (
<>
<Button
data-testid="titleButton"
variant="contained"
onClick={handleClickOpen}
>
{Title}
</Button>
<Dialog open={openDialog} onClose={handleClickClose}>
<DialogTitle>{Title}</DialogTitle>
<DialogContent>
<DialogContentText>
<Typography sx={{ mb: 1 }}>{Desc}</Typography>
{List && (
<ul>
<Stack spacing={1}>
{List.map((item, i) => (
<li key={i}>{item}</li>
))}
</Stack>
</ul>
)}
</DialogContentText>
<DialogActions>
<Button
data-testid="closeButton"
variant="contained"
onClick={handleClickClose}
>
Close
</Button>
</DialogActions>
</DialogContent>
</Dialog>
</>
);
};

export default ButtonDialog;

Debugging:

Even with access to the component’s source code, you must be certain of what is being mounted to the DOM to write accurate tests, and the tool I recommend to accomplish this is the debug method. In software development, debugging is a process by which the source of defects or problems preventing proper code execution are identified and resolved. This applies to our testing suite because we must ensure accuracy in assessing Happy/Sad test situations. React Testing Library (RTL) — has several different features that facilitate debugging, like ‘Auto Logging’ which prints the state of the container (DOM) to the console following a failing test. The goal is to write tests that pass/fail as expected so let’s explore the proper implementation of the debug method in our tests to avoid having to rewrite them after failure.


describe("ButtonDialog testing suite", () => {

it("ButtonDialog does NOT render close button when closed", () => {

const { container } = render(
<ButtonModal data={{ id: "", Title: "", Desc: "", List: "" }} />
);

// eslint-disable-next-line testing-library/no-debugging-utils
screen.debug(container);

});

});

Once you set up the debugger within your test and run your suites’ code, you’ll see the component tree printed in the console just as it appears in the DOM. Now, we can easily inspect the logged printout to pinpoint the element attribute we want to target with our Happy Path test. Our Sad Path test will check for those code structures that aren’t present on the initial component mount, and we can now accurately implement this negative test with the debug printout in the console.

One little thing to mention is the es-lint error or red squiggly lines that might appear in your suite when utilizing the debug method. Don’t be alarmed or feel like you did something wrong, you can deactivate the rule for that line, that entire file, or follow other steps to remove the rule from your react testing library configuration completely. You can find more on that particular rule here. I went with disabling the rule for the single line, but for your information, the code will still run even without disabling the rule.

// debugger print out to console
<div>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium css-sghohy-MuiButtonBase-root-MuiButton-root"
data-testid="titleButton"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
</div>

Now that we know beyond the shadow of a doubt, what is and isn’t being rendered we can easily test the component. I will show both testing paths Happy and Sad to give you an example of the entire suite.

ButtonDialog.test.js

describe("ButtonDialog testing suite", () => {

// Happy Path

it("ButtonDialog successfully renders with title button", () => {

render(
<ButtonDialog
data={{ id: "", Title: "button test", Desc: "", List: "" }}
/>
);

const component = screen.getByTestId("titleButton");

expect(component).toBeTruthy();

expect(component).toHaveTextContent("button test");
});

// Sad Path

it("ButtonDialog does NOT render close button when closed", () => {

render(
<ButtonDialog data={{ id: "", Title: "Hello", Desc: "", List: "" }} />
);

const closeButton = screen.queryByTestId(/closeButton/i);

expect(closeButton).toBeNull();

});

});

AAA Breakdown

For our Happy Path, we arrange our test by mounting the ButtonDialog component to the DOM via the render function. We then act by creating a variable called component and setting it to the value of the getByTestId method. This method searches the component for an element with a test id matching the string titleButton. Finally, we assert that our component variable will have a truthy value, and then go further by checking for the text content we passed into the component props, “button test”.

For our Sad Path, we arrange our test exactly as we did in the Happy Path test, by mounting our component via the render method. We then act, creating the variable closeButton and setting it to the return value of the method queryByTestId. The final step in the test is to assert that our variable will not be present by expecting the result to be null or falsy.

If you look closely at the act stage for both Happy and Sad paths you will see two different query methods getByTestId and queryByTestId.Both methods search the mounted component tree for an element with a specific test id but the difference is how the two methods fail. The getBy methods fail by throwing an error if the target node is not found, while queryBy methods return null. Additionally, the react testing library docs directly stated that queryBy queries are useful for asserting an element is not present, so I followed the suggestion.

Once both testing paths are constructed, run the command npm test ButtonDialog and you’ll have a passing suite printout like the one below.

Wrapping Up

We’ve headed down a new path and gone from Happy to Sad but continuing to utilize this AAA format gives me a good organized feeling. A little sidenote — after researching to write this article, I have deduced that it is wildly inaccurate to interchange “component” and “unit” when referring to software testing even if I build functional components, lol. In part 3 we will explore mocking component utilities, dependencies, and user interactions ( a button click ) in our suites. Hope to see you then.

Happy Coding Yall!

Links

Software Testing:

Sad Path:

Debugging:

--

--