Advice

What is TDD? What is Unit Testing?

John Sonmez · Jan 30, 2017 · 12 min read

I have a love / hate relationship with test driven development and unit testing.\n\nI’ve been both an ardent supporter of these “best practices,” but I’ve also been more than skeptical of their use.\n\nOne of the big problems in software development is when developers—or sometimes managers—who mean well apply “best practices” simply because they are best practices and don’t understand their reason or actual use.\n\nI remember working on one such software project where I was informed that the software we were going to be modifying had a huge number of unit tests––around 3,000.\n\nNormally this is a good sign.\n\nThis probably means the developers on the project implemented other best practices as well, and there is going to be some semblance of structure or meaningful architecture within the code base.\n\nI was excited to hear this news since it meant that my job as the mentor / coach for the development team was going to be easier. Since we already had unit tests in place, all I had to do was get the new team to maintain them and start writing their own.\n\nI opened up my IDE and loaded the project into it.\n\nIt was a big project.\n\nI saw a folder labeled “unit tests.”\n\nGreat. Let’s run them and see what happens.\n\nIt only took a few minutes and—much to my surprise—all the tests ran and everything was green. They all passed.\n\nNow I really became skeptical. Three thousand unit tests, and they all passed?\n\nWhat is going on here?\n\nMost of the time when I am first pulled onto a development team to help coach them, there are a bunch of failing tests if there are any unit tests at all.\n\nI decided to spot check one test at random.\n\nAt first glance, it seemed reasonable enough.\n\nIt wasn’t the best, most explanative test I’d ever seen, but I could make out what it was doing.\n\nBut then I noticed something…\n\nThere was no assert.\n\nNothing was actually being tested.\n\nThe test had steps and those steps were running, but at the end of the test where it is supposed to check something, there was no check.\n\nThe “test” wasn’t testing anything.\n\nI opened up another test.\n\nWorse.\n\nThe assert statement, which was testing something at some point, was commented out.\n\nWow, that’s a great way to make a test pass; just comment out the code that’s making it fail.\n\nI checked test after test.\n\nNone of them were testing anything.\n\nThree thousand tests and they were all worthless.\n\nThere is a huge difference between writing unit tests and understanding unit testing and test-driven development.\n\n

What Is Unit Testing?

\n\nThe basic idea of unit testing is to write tests which exercise the smallest “unit” of code possible.\n\nUnit tests are typically written in the same programming language as the source code of the application itself and written to utilize that code directly.\n\nThink of unit tests as code that tests other code.\n\nWhen I use the word “test” here, I’m using it fairly liberally because unit tests aren’t really tests. They don’t test anything.\n\nWhat I mean by this is that when you run a unit test, you don’t typically find out that some code doesn’t work.\n\nIt’s when you write a unit test that you find that information out.\n\nYes, the code could change later and that test could fail, so in that sense, a unit test is a regression test. In general, however, a unit test is not like a regular test where you have some steps you are going to execute and you see whether the software behaves correctly or not.\n\nAs a developer writing a unit test, you discover whether the code does what it is supposed to or not while you are writing the unit test because you are going to be continually modifying the code until the unit test passes.\n\nWhy would you write a unit test and not make sure that unit test passes?\n\nWhen you think about it this way, unit testing is more about specifying absolute requirements for specific units of code at a very low level.\n\nYou can think of a unit test as an absolute specification.\n\nThe unit test specifies that under these conditions with this specific set of input, this is the output that I should get from this unit of code.\n\nTrue unit testing tests the smallest cohesive unit of code possible, which in most programming languages—at least object oriented ones—is a class.\n\n

What Is Sometimes Called Unit Testing?

\n\nOftentimes, unit testing is confused with integration testing.\n\nSome “unit tests” test more than one class or test larger units of code.\n\nPlenty of developers will argue that these are still unit tests since they are whitebox tests written in code at a low level.\n\nYou shouldn’t argue with these people.\n\nJust know in your mind that these are really integration tests and that true unit tests test the smallest unit of code possible in isolation.\n\nAnother thing that is often called unit testing—but isn’t really anything at all—is writing unit tests that have no assert. In other words, unit tests that don’t actually test anything.\n\nAny test, unit test or not, should have some kind of check—we call it an assertion—at the end that determines whether it passes or fails.\n\nA test that always passes is useless.\n\nA test that always fails is useless.\n\n

The Value of Unit Testing

\n\n\n\nWhy am I such a stickler on unit testing?\n\nWhat is the harm in calling unit testing real testing and not testing the smallest unit in isolation?\n\nSo what if some of my tests don’t have an assert? They are at least exercising the code.\n\nWell, let me try and explain.\n\nThere are two major benefits, or reasons, to perform unit testing.\n\nThe first one is to improve the design of the code.\n\nRemember how I said unit testing is not actually testing?\n\nWhen you write proper unit tests where you force yourself to isolate the smallest unit of code, you find problems in the design of that code.\n\nYou might find it extremely difficult to isolate the class and not include its dependencies, and that might make you realize that your code is too tightly coupled.\n\nYou might find that the basic functionality you are trying to test is spread out across multiple units, and that might make you realize that your code is not cohesive enough.\n\nYou might find that you sit down to write a unit test and you realize—and believe me, this happens—that you don’t know what the code is supposed to do, so you can’t write a unit test for it.\n\nAnd, of course, you might find an actual bug in the implementation of the code as the unit test forces you to think about some edge cases or test multiple inputs which you may not have accounted for.\n\nBy writing unit tests and strictly adhering to having them test the smallest units of code in isolation, you find all kinds of problems with that code and the design of those units.\n\nIn the software development lifecycle, unit testing is more of an appraisal activity than a testing one.\n\nThe second main purpose of unit testing is to create an automated set of regression tests which can operate as a specification for the low level behavior of the software.\n\nWhat does that mean?\n\nWhen you change shit, you don’t break shit.\n\nIn that way unit tests are tests: regression tests.\n\nBut the purpose of unit testing is not to merely build these regression tests.\n\nIn the practical world, very few regressions are caught by unit tests since changing the unit of code you’re testing almost always involves changing the unit test itself.\n\nRegression testing is much more effective at the higher level as a black box testing activity because, at that level, internal structure of the code could be changed while the external behavior is expected to remain the same.\n\nUnit tests test the internal structure, so when that structure changes, the unit tests don’t “fail.” They become invalid and have to be changed, thrown out, or rewritten.\n\nNow you know more about the true purpose of unit testing than most 10-year software development veterans.\n\n

What Is Test-Driven Development (TDD)?

\n\n\n\nRemember the chapter where we talked about software development methodologies, and the waterfall methodology often didn’t work out practically because we never had complete specifications up front?\n\nTDD is the idea that, before you write any code, you write a test that acts as a specification for exactly what that code is supposed to do.\n\nThis is an extremely powerful concept in software development, but is often misused.\n\nTDD usually means using unit tests to drive the creation of the production code being written, but it can be applied at any level.\n\nFor the purposes of this chapter, though, we are going to stick with the most common unit testing: application.\n\nTDD flips things around so that instead of writing the code first and then writing unit tests to test that code, (which we know isn’t the case anyway), you are going to write the unit test first and then write just enough code to make that test pass.\n\nIn this way, the unit test is “driving” the development of the code.\n\nThis process is repeated over and over.\n\nYou write another test that defines more functionality of what the code is supposed to do.\n\nYou change the code or add code to make the test pass.\n\nFinally, you refactor the code—or clean it up—to make it more succinct.\n\nThis is often called “Red, Green, Refactor” because at first the unit test fails (red), then code is written to make it pass (green), and finally the code is refactored.\n\n

What Is the Purpose of TDD?

\n\nJust like unit testing itself can be a best practice that is misapplied, TDD can be as well.\n\nIt’s very easy to call what you are doing TDD and to even follow the practice and not understand why you are doing it or the value—if any—it is providing.\n\nThe biggest value of TDD is that tests happen to make excellent specifications.\n\nTDD is essentially the practice of writing unambiguous specifications, which can be automatically checked, before writing code.\n\nWhy are tests such great specifications?\n\nBecause they don’t lie.\n\nThey don’t tell you your code should work one way and then tell you after you spend two weeks pounding Mountain Dew and get everything working it should actually work another way and “it’s all wrong; that’s not what I said at all.”\n\nTests, if properly written, either pass or fail.\n\nTests specify in no uncertain terms exactly what should happen under a certain set of circumstances.\n\nSo, in that respect, we could say the purpose of TDD is to make sure we fully understand what we are implementing before we implement and that we “got it right.”\n\nIf you sit down to do TDD and you can’t figure out what the test should test, it means you need to go ask more questions.\n\nThe other value of TDD is in keeping the code lean and succinct.\n\nCode is costly to maintain.\n\nI often joke that the best programmer is the one who writes the least code or even finds way to delete code because that programmer has found a surefire way to reduce errors and to decrease the maintenance cost of the application.\n\nBy utilizing TDD, you can be absolutely sure that you do not write any code that is not necessary since you will only ever write code to make tests pass.\n\nThere is a principle in software development called YAGNI, or you ain’t going to need it.\n\nTDD prevents YAGNI.\n\n

A Typical TDD Workflow

\n\nIt can be a little difficult to understand TDD from a purely academic perspective, so let’s explore what a sample TDD session might look like.\n\nYou sit down at your desk and quickly sketch out what you think will be a high-level design of a feature to allow a user to login to the application and change their password if they forget it.\n\nYou decide that you are going to start off by first implementing the login functionality by creating a class that will handle all the logic for doing the login process.\n\nYou open up your favorite editor and create a unit test which is called “Empty login does not log user in.”\n\nYou write the unit test code that first creates an instance of a Login class (which you haven’t created yet).\n\nThen, you write some code to call a method on the Login class that passes in an empty username and password.\n\nFinally, you write an assertion, or assert, which asserts that the user is indeed not logged in.\n\nYou attempt to run the test, but it doesn’t even compile because you don’t have a Login class.\n\nYou remedy that situation by creating the Login class along with a method on that class for logging in and another for checking the status of a user to see if they are logged in.\n\nYou leave the functionality in this class and methods completely empty.\n\nYou run the test and this time it compiles, but quickly fails.\n\nNow, you go back and implement just enough functionality to make the test pass.\n\nIn this case, it would mean always returning that the user is not logged in.\n\nYou run the test again, and now it passes.\n\nOn to the next test.\n\nThis time you decide to write a test called “User is logged in when user has valid username and password.”\n\nYou write a unit test that creates an instance of the Login class and try to login with a username and password.\n\nIn the unit test, you write an assertion that the Login class should say the user is logged in.\n\nYou run this new test, and of course it fails because your Login class always returns that the user is not logged in.\n\nYou go back to your Login class and implement some code to check for the user being logged in.\n\nIn this case, you’ll have to figure out how to keep this unit test isolated.\n\nFor now, the simplest way to make this work is to hardcode the username and password you used in your test and if it matches, then you’ll return that the user is logged in.\n\nYou make that change, run both tests, and they both pass.\n\nNow you look at the code you created, and see if there is a way you can refactor it to make it more simple.\n\nSo on you go, creating more tests, writing just enough code to make them pass, and then refactoring the code you wrote until there are no more test cases you can think of for the functionality you are trying to implement.\n\n

These Are Just the Basics

\n\n\n\nSo, there you have it.\n\nThose are the basics of TDD and unit testing—but they are just the basics.\n\nTDD can get a bit more complex when you truly try and isolate units of code because code is connected together.\n\nVery few classes exist in complete isolation.\n\nInstead, they have dependencies, and those dependencies have dependencies and so on.\n\nTo handle situations like these, veteran TDDers make use of mocks, which can help you to isolate individual classes by mocking the functionality of dependencies with pre-setup values.\n\nSince this is a basic overview of TDD and unit testing, we won’t go into details here about mocks and other TDD techniques, but just be aware that what I presented in this chapter is a somewhat simplified view.\n\nThe idea is to give you the basic concepts and the principles behind TDD and unit testing, which hopefully you now have.\n\n


\n\n

John Sonmez

John Sonmez

John Sonmez is the founder of Simple Programmer, author of "The Complete Software Developer's Career Guide" and "Soft Skills: The Software Developer's Life Manual." He helps software developers build remarkable careers.