TDD Tests as “Karen”s
Categories:
TDD
Categories: TDD
We’ve all heard the meme by now: The Karen. Usually a blonde woman with an eccentric hairstyle that demands to see the manager when things do not go as they want them to. Personally I think the term is a bit sexist as I’ve met plenty of men who act this way too. Anyway, when you’re stuck behind such a person in line at a retail establishment, you suffer while they selfishly demand things until someone relents, and you have to wait until they get their way. I don’t much care for those people. But when it comes to TDD, I want the tests I write to be just like that. I want them to be demanding, picky, and downright relentless. Let me explain why. Let’s say you are not a TDD shop. You are tasked with this requirement: Every night at midnight, the transactions that have been recorded throughout the day (let’s say, they are financial in nature) must be committed to a service. Assuming we know what it means to “commit a transaction” this seems pretty simple, right? You’d create a job running on a timer, and when midnight comes you’d activate the code that commits the collection of transactions. Most developers could write that pretty quickly and move on. Let’s say you did that. The next day you come in to find the business in an uproar. The transactions did not go through, and your customers have sustained significant financial damage as a result. There is liability, potential loss of market share, and damaged reputation. When you ask why this happened, they tell you that the commit process was “too late” and that the transactions were rejected as a result. The problem is the notion of “happening at midnight”. That seems like a perfectly clear, simple requirement with little room for misinterpretation. Everyone knows what midnight it. But nothing can happen “at midnight”. Midnight is an infinitely short period of time… the moment it becomes midnight, it is immediately “after midnight”. So what did the client actually want? Did they mean “by midnight”? If so, then you should have started the commit process prior to midnight so that it will compete before the deadline? But how much earlier should you start? Is that based on the speed of the computer performing the action? Is there a time that is “too early” in terms of regulations or business rules? You now know there is a “too late”, so that seems believable. Is it really “at” midnight, or “by” midnight, or what? Now imagine you are a TDD team, and you received that same requirement in the same language. The first thing you must do is convert it into some kind of executable test, probably a unit test if you are a developer. When such a test is written, it must express three things:
TDD says “until you have these three things accomplished in a test, and have run that test and observed its failure, you may not begin the development work.” It will not budge, or yield, or get out of line until you do what it wants. It’s a Karen, or whatever the non-sexist term might be. TDD is a discipline, and it only works if you follow it diligently. How will you create the environment that the behavior applies to? Do you really want to create a bunch of bogus transactions every time you need to run the test? How can you activate the commitment if it’s based on the system clock? Will you have to come in every night at midnight to conduct your test? How will you know if it worked? What might make it not work? What does “it worked” even mean? In TDD, the team must know how to do all these things. Some of it involves technique (dependency injection, mocking, endo-testing, etc…), some of it requires that we ask questions that we might otherwise neglect to ask (like “how will I know if it worked?”) and some of it will challenge the design of the system we are planning. Should the “trigger at midnight” code be intertwined with the “commit the transactions” code? Probably not, but that would be an easy mistake to make. TDD won’t let you, because it will be too painful and arduous to write a test of such a thing. Bad designs are hard to test. Is your design any good? If not, don’t you want to know? It’s hard if not impossible to write a test for something you don’t understand. Do you really know enough to build this feature? Are there questions you should be asking? TDD works because, among other things, it is demanding, picky, and downright relentless. It is that annoying customer that won’t go away until you address them, and thus it holds your feet to the fire in a way that makes you better at your job, and more valuable to your clients. To do this, the team must be effectively trained. Just knowing “how to write a unit test” won’t do it, not even close. If you do not know how to control dependencies (like “time” in the example) or how to separate behaviors in your system without over-complicating its design, or what it means to completely express a requirement as a test, then you’ll struggle and probably give up. But if the team is empowered with this knowledge they can move swiftly, confidently, and aggressively to ensure that the organization they serve remains competitive in an ever-changing world. TDD makes you do it right, because doing it wrong simply won’t work. And isn’t that great? |
ATDD and TDD
A question that we are often asked is: “What is the difference between Acceptance Test Driven Development (ATDD) and Test Driven Development (TDD)?” These two activities are related by name but otherwise seem to have little to do with each other.
This is a human-oriented interaction that focuses on the customer, identifying their needs. These needs are specified using the external, public interfaces of the system.
All requirements coming from all of these different customers must be addressed, identified and expressed through the ATDD process. For example:
And let us not forget the the developers are customers too, who else do we build tracers and loggers for? This is an obvious, publicly visible facet of the developer’s work. But when do we need these facilities? When we try to fix bugs. When we want to understand how the system works. When we work on the system for any reason. |
The Importance of Test Failure
The typical process of Test-Driven Development goes something like this:
There are variations (we’ll suggest a few in another blog), but this is fairly representative. We create process steps like this to guide us, but also to create agreement across the team about what we’re going to do and when we’re going to do it. Sometimes, however, it seems unnecessary to follow every step every time, rigidly, when we’re doing something that appears to be simple or trivial.
So: test failure validates that the test is meaningful and unique, and it also confirms that the code we’re about to write is useful and necessary. For such a simple thing, it provides an awful lot of value.
In TDD, success is not getting to green. Success is the transition from red to green.
[1] And we apologize for the fact that you are now conscious about your breathing. It’ll pass. |
Mock Objects, Part 1
Narrow Specifications
If the behavior we are testing/specifying has dependencies on any of these “other” things then we must take control of them in the test so that the only thing we are focusing on is the one thing we are not controlling. Mocks are a big part of solving this problem.
This, of course is a crash test dummy. This is a good analogy for a mock object for two reasons.
+CheckFieldBoundary() is what we want to specify/test. If the GPS reports our location is too close to leaving the planting area, then the BoundaryAlarm object should call ActivateDashLight() on the DashLight interface.
Note that the mocks have additional methods added to them that are not defined in the interfaces they mock:
These extra methods for conditioning and inspecting the mocks will not be visible to the BoundaryAlarm object because the mock instances will be implicitly up-cast when they are passed into its constructor (assuming a strongly-typed language). This is is essentially encapsulation by casting.
Adding more capability to this mock later, or even creating another mock for the purpose of specifying the deactivation of the light, will not be hard to do. What is hard is keeping track of capabilities we may build in anticipation of a need, when we do not know if and when that need will arrive. Also, if this “just in case” capability is actually wrong or non-functional, we can easily fail to notice this or lose track of it.
|
Mock Objects, Part 2
Techniques
Perhaps the DashLight implementation is part of our responsibility, and so we’ve opted to create a separate interface which makes mocking easy. The GPS team might not have (note the GPS object above is a concrete implementation which we are directly dependent upon). To test our object (BoundaryAlarm) we must bring the GPS under the control of the test.
Here we have simply sub-classed the “real” GPS class. As was the case when GPS was an interface, our mock will up-cast and appear (to BoundaryAlarm) to be the real thing. The extra method SetCurrentLocation() will be available to the test, for conditioning purposes, because the test will hold the reference to the mock in a down-cast to its actual type.
Obviously if we could change the GPS class, we’d opt to create that separate interface and eliminate all of these problems. But what if we cannot?
GPSWrapper is our class, which we created just for this purpose, so we could mock it. Clearly we would have to change BoundaryAlarm in this case, as it no longer directly depends on GPS but rather on the GPSWrapper, and these two are not interchangeable. But, if we can change BoundaryAlarm and cannot change GPS, then this technique is appropriate.
This eliminates the inheritance coupling between the mock and the implementation entirely, and thus we create no instance of the actual wrapper implementation (GPSWrapperImp) at test time. Our tests will run fast, be completely repeatable, and will not require the actual GPS system to run. These are all techniques for creating mocks by hand. Another approach is to use a mocking framework, a tool that creates these mocks for you. We’ll examine an example of such a tool a bit later on, and also discuss what we like and dislike about the use of such tools. In any case, whether you automate you mocks or write them by hand, every developer should know how to handcraft them, how they work, and what they do.
Part 1: https://www.projectmanagement.com/blog-post/68267/Mock-Objects--Part-1 Part 2: https://www.projectmanagement.com/blog-post/68254/Mock-Objects--Part-2 Part 3: https://www.projectmanagement.com/blog-post/68253/Mock-Objects--Part-3 |