On this episode of React Wednesdays, we talk with Eric Elliott about the questions every unit test must answer and how to craft more modular, resuable and debuggable code.
You can find more information on our web, so please take a look.
Finally, Unit Testing is no longer that thing you said you’d eventually get to, and purposely avoid. Eric Elliott dropped a masterclass in testing, complete with demonstrations and live coding, covering things like “The Five Questions Every Unit Test Must Answer,” the importance of RITE, and how testing makes you a better software developer. Eric’s interview is rich with wisdom and clarity.
On our October 7th, edition of React Wednesdays, we hosted Eric Elliott, Jesse, and Sergio to discuss Unit testing in React. During the show, we discussed the finer points of Unit Testing and watched a live demonstration of building unit tests for a React application. This show was especially interesting to me and my colleagues at Digital Primates because we use Eric’s materials in our training program for new consultants who work on Enterprise React applications. The full recording of our show on React Unit Testing with Eric Elliott is available on YouTube.
Eric Elliott is a prolific author, mentor, advisor, and developer. As part of his work, he runs DevAnywhere, a live 1:1 remote mentorship program using video meetings with screen sharing and hands-on coding. Jesse is a member of the program and also serves as a mentor. Sergio is an active mentee in DevAnywhere and gives great code reviews.
Eric opened the discussion with a concise masterclass in proper test design (1:51). Many developers agree testing is necessary, but don't yet understand how to write quality tests. Eric's advice is clear and approachable and would fit on a sticky note. React developers would do well to place this on their monitor for reference.
His first point is to make components testable through proper component design. React provides mechanisms to separate visual elements from business logic and state. Components with properly separated concerns are easier to test, and easier to use and reuse. He then explained the five questions every unit test must answer.
Eric developed a mental guide to help software developers ensure they write tests of good quality. He summarized the points into five questions.
He explains the thinking behind each point, as well as why each is important.
Another point for the sticky-note is Eric's acronym on the elements of a good test. This acronym is RITE:
Many software developers who are new to testing have a hard time reconciling testing with their other work. At times, testing can feel orthogonal to writing the code that ships. Developers usually feel pressed for time and feel the urge to skip testing. I asked Eric to offer advice to developers who shared that sentiment (10:51).
Eric explained that writing testable code encourages modularity, and modular software is most often better designed and implemented. Modularizing the code for testing helps the software developer build software that is easy to maintain and components that are easier to use.
From the management perspective, Test Driven Development practices grow confidence in development teams to ship code with less fear that an unexpected side effect will impact production.
Further, Eric says a good test suite is the best technical documentation you can have because it is granular and guaranteed not to get out of sync with the code, unlike written documentation does.
Jesse started a live coding demonstration of building and testing code (15:49). His example consisted of two components:
We watched Jesse build the two components and add appropriate tests for the next 20 minutes. During the process, he and Eric explained the source code and the reasons and strategy behind each decision.
Jesse used RITEway, an open-source library for simple, readable, helpful unit tests that conform to the "Five Questions." RITEway forces you to write Readable, Isolated, and Explicit tests, because that's the only way you can use the API. The RITEway method of writing tests is very straightforward and easy to read. It also scales well as tests accumulate. RITEway looks to be a beneficial library to structure and run tests. If you are new to tests or are interested in RITEway, be sure to watch this segment. There are many interesting sections like:
Unit testing is vital to ensuring high code quality. Unit testing also helps development teams maintain their feature shipping velocity. Even as the code base grows in size and complexity, the test coverage reduces unknown side-effects from code changes so software development teams can ship without worry.
To close out the show (56:14), I gave my impression that building unit tests with RITEway was very easy to do and didn't distract at all from the development work. Eric replied with what might be the most poignant advice:
The benefits [Of TDD] are monumental... I started to get a lot faster with TDD than before I started using TDD even though I'm writing two, three, or four times more code. It's not typing the characters out that is time-consuming. What is time-consuming about programming is thinking through what a component should do, how it should relate to other system components, and the system architecture. TDD helps you think through this process in a way to write more modular, more reusable, and more debuggable code. —Eric Elliott
We learned a lot from our time with Eric, Jesse, and Sergio and are glad to make the recording available for you on the React Wednesdays YouTube Playlist. If you'd like more information, check out Eric's book Composing Software, and Eric's YouTube Channel. If you’d like to stay current on topics like these, consider subscribing to the Enterprise React Newsletter. New issues come every two weeks.
If all of this sounds interesting and you are looking for a way to level up your testing and software development skills, check out DevAnywhere. Eric designed DevAnywhere to elevate professional developers to the highest levels of technical excellence. The mentorship offerings are suitable for anybody building JavaScript applications or leading JavaScript development teams. As an initially self-taught software developer, I appreciate how 1:1 sessions can adapt to fit particular needs.
In this day and age, unit testing isn’t as controversial as it once was. Sure, you still see the occasional inflammatory, clickbait-y, confrontational “unit testing is garbage” type of post on Reddit and Hacker News.
And there are still developers out there ignorant of the existence of unit testing—or any type of automated testing, for what it’s worth. Believe it or not.
However, if we consider the developers that do know about unit testing, I’d say the debate is pretty much over. It’s generally agreed, today, that unit testing has a positive influence on software projects.
With that in mind, the next question then becomes “what makes for a good unit test?” That’s what this post is all about. Today, we present you five must-haves for a great unit test.
The first must-have for your unit testing suite is speed. I’m far from being the first one to say that, even though I’d be pleased to be the last. Household names in the industry have told us the same thing time and time again.
A slow test suite will discourage developers from running it. By not running the tests as often as possible, the developers will miss out on what’s arguably the primary benefit of unit testing: the confidence to change the code without fear.
Why are the tests slow? It could be that the tests interact with real external dependencies (e.g., the database, the filesystem, etc.) instead of faking them. Or it even may be that some algorithm was implemented inefficiently.
But it also could be that the architecture of the application isn’t clean enough. Maybe there’s too much coupling and too many dependencies, making it impossible for the test code to exercise a unit in complete isolation.
If this is the case, the slowness of the tests is doubly evil: it’s both a symptom of already-existing problems and the cause for new ones to come.
What’s the adequate number of assertions per test method? As many as you wish? Just one? Blind adoption of an extreme viewpoint is always…well, extreme, but it also amounts to cargo cult programming, and we know better than that by now.
The only reasonable answer to the “how many assertions per test method” question then becomes apparent: it depends. The correct number of assertions might be one for this test but three for that other and then two for another. You must use as many assertions as you need to correctly exercise that specific scenario for the unit of work under test.
Goto UnitedTest to know more.
Let’s see a quick example. Suppose the following snippet is a test method for a custom stack class:
As you can see, the test has two assertions. Is it doing too much? Has it lost its focus? No, it isn’t, and no, it hasn’t. There are two assertions, but they’re both employed in documenting the same scenario. In other words, both assertions help tell the same story:
What do we take from this? The test starts losing its focus when it tries to tell more than one story in a single test case.
Your unit tests must be reliable. You must be able to trust them. Otherwise, you and your team won’t have the confidence to refactor fearlessly. Then it’s virtually guaranteed the unit tests will become less of a good thing to cherish and more of a burden to loathe.
And how do you trust your tests? Easy. You make them trustworthy. You make them reliable.
Your tests shouldn’t just start failing out of the blue. If the tests were passing yesterday and no changes were made to the code, then they should still pass today. If the tests you wrote for your open-source project work on your computer in the US, then they should also pass on the computer of a Japanese contributor. The same goes for a Turkish, Brazilian, or French contributor, of course.
When a unit test starts randomly failing, that’s a sign that it depends on things it shouldn’t depend on. It’s making assumptions about the environment it’s running on, its timezone, culture, or other constraints.
Let’s drive the point home, using a quick example. Consider the following snippet:
It’s a quintessential value object: a distance class. It’s a very rudimentary implementation, though. A real implementation would have factory methods for other units, methods for performing arithmetic, and so on. But for our example, it will suffice.
Consider now the following test:
“What’s the problem with the test?” you might wonder. Don’t keep wondering. Fire up Visual Studio right now, create a new solution, copy and paste the code, run the test, and find out.
If I had to guess, I’d say the test passed…if you’re in the United States. Or Mexico, Israel, Japan, and many more countries. If, on the other hand, you’re in Brazil, France, Germany, or Argentina, among several others, the test above will fail!
And why is that? Simple. The countries in the latter list use a comma as decimal separator. Usually, the overload of ToString that doesn’t take an IFormatProvider will default to the current culture. And that’s exactly what happens when we call this method on “meters.” The test above is flawed because it implicitly assumes a culture where the dot is used for decimal separators.
The fourth must-have for unit tests is independence. This one is strongly related to the previous must-have. In fact, you can think of it as a special case of reliability. And what should your unit tests be independent of?
Another unit test.
A unit test should never depend on another test. One test should never do any preparation for the next test, nor should it expect another test to do the same for it.
We can go further and say that this vocabulary is flawed. It doesn’t make sense to talk about a “previous” and “next” test since a real unit test should be completely unaware of the existence of other tests. From its point of view, it is the only test.
And what is all this independence good for? First of all, it adds to the overall reliability of the suite. Let’s say we have tests A, B, and C. A developer makes a change to some code that is exercised by A. Then A starts failing—and rightly so since the developer inadvertently introduced a bug with their changes.
Should B and C fail as well? If they don’t exercise the changed code, they shouldn’t. But let’s say that right after its assertion, A sets up some values that are used by both B and C. Now when A fails, the code after the assertion doesn’t run, the values aren’t set up, and B and C start to fail.
Of course, such an example isn’t even really needed when you think about it. With such a dependency in place, it would be impossible to run B or C on their own without first running A as well.
Creating dependencies between tests is a recipe for disaster. It makes for brittle tests, which require complicated setups and, as a consequence, fail randomly and aren’t run as often as they should be. Keep your tests wholly isolated and independent, both from external dependencies and from one another.
Last but not least, I give you the really-must-have of unit testing: simplicity. Be dead simple when writing tests. Resist the temptation to get fancy. You know when you’re writing a test and you hardcode a value and immediately think, “Hmm, it’s so ugly to hardcode this value here. I could generate it on the fly!”
Please, don’t!
That’s precisely the kind of thing that leads to untrustworthy tests. When you try to go the fancy route and generate values instead of hardcoding them, you risk duplicating the implementation code in the test code. And if you’re unlucky enough to have gotten it wrong in both places, you know what’ll happen? The implementation will be wrong, but the test will pass. Which frankly is worse than having no tests at all!
Keep your test code dead simple. No “if” statements. No loops. Just do the good old Arrange-Act-Assert thing, and you’re on the right track.
“But I really need to loop/make a decision in a test. What can I do???”
Do you feel the urge to use an “if” statement in a test? Chances are you should split it into two tests. But if what you really wish for is to loop through a series of values, consider if NUnit’s parametrized tests won’t do the trick.
If nothing helps, then create a utility class, move all the fancy stuff to this new class, and then use it on your tests. It should go without mention that the utility class itself should be tested, right?
Do you write unit tests? If so, then congratulations! But if the answer is “no,” then please consider starting ASAP. You’ve already seen studies demonstrating the benefits of unit testing. Now we’ve just shown you the qualities a great test must have.
The beginning can be rough, but it’s worth it. The benefits of testing far outweigh the costs. And the sooner you begin, the more you get to practice and the sooner you’ll reap the rewards.
See you next time!
If you are looking for more details, kindly visit United Test.