So let’s just dive in today. Our current test class is fairly clean and straightforward, but we’re not using all of Moq’s capabilities yet. One notable thing is that we have to remember to call VerifyAll on all of our individual mocks. Fortunately, Moq provides us with a tool to mitigate this problem: MockRepository is a class we can use to create and track mocks, and verify all expectations at once.
In the real world, Factories manufacture items for output. In software engineering, Factories create object instances for us so we can avoid using the “new” operator. Factories also give us substantially more control when testing, and can even provide a means of verifying “Black box” operations.
Why would we want to avoid the “new” operator? Isn’t that what it’s there for?
Well, yes, but newing up a dependency within a class goes against the concept of dependency injection. Thinking back to our ReallyHardToTest class, we know we’re creating a new instance of SomeModel in the constructor, and one of our methods provides a way to change the SomeModel instance. The problem is that since we create the instance within ReallyHardToTest, we cannot control it or even verify that an instance is created. Furthermore, we can’t verify that the SetFilename method actually does what it says on the tin because the SomeModel instance isn’t exposed publicly by that class.
Factories Help Us Build Things
At its root, the factory pattern (there are actually a few variants) encapsulates the logic for creating instances of particular types. When employing these patterns, now dependent classes can have a factory injected, and then ask the factory for a new instance when they need it. Since the factory is injected, we can control it at test time – we even get to control the instance returned by the factory!
In advanced scenarios, for example when a factory will be invoked repeatedly, we can use advanced features of Moq, including SetupSequence() to return different instances in a specific order.
Challenge: Classes with Static Dependencies are Difficult to Test
Static Dependencies pose a set of challenges to testability, most notably:
- Static Dependencies cannot be mocked
- Due to their nature, static dependencies are rarely injected so they are hidden dependencies
In my prior post, I showed a very difficult-to-test class that uses the static System.IO.Path.GetFileName method. Today, we’ll do some refactoring on that class to make it more testable!
My students often ask me “How can I tell if my code is too complex?” There are a lot of different answers to that question, but I thought I would share my favorite complexity detection tool with you – a tiny purple stuffed monkey:
The basic concept behind it all is to explain your code to your duck/monkey/other inanimate object, step by step. If it takes you longer than 1-2 minutes to explain a method, it’s probably too complex and should be refactored to something simpler.
Well that sounds great and all, but how exactly do I reduce a method’s complexity?
Good catch – “Just simplify it” is unacceptably hand-wavey and doesn’t really provide much guidance. One good approach for reducing complexity is called the Single Level of Abstraction Principle (SLAP). As might be apparent from the name, SLAP recommends that all statements within a given method exist at the same “Level of Abstraction”.
OK, but what’s a Level of Abstraction?
Nobody likes to fail. Such a simple word implies so many negative connotations: Laziness, incompetence, unprofessionalism, and more. With that in mind, it’s interesting to consider the fact that Unit Tests are all about failure!
Fail Early, Fail Hard
You might be scratching your head at this point, wondering what I mean by that. Aren’t unit tests supposed to pass? As usual, the answer is a bit deeper than the surface. Much like the canaries that miners used to carry with them to detect noxious gases, unit tests serve as an early indicator that something is wrong. In a traditional TDD workflow, failing tests are a normal occurrence (in fact, with strict TDD, you start with a failing test). The test runner makes failures clearly visible after each test run. This is a good thing because it reduces the likelihood of overlooking a failing test case.
In fact, unit testing’s primary value comes from failing tests, detecting bugs as early as possible in the development cycle. Multiple studies have shown that the cost of fixing software bugs rises dramatically the later the defect is discovered. To illustrate, let’s look at a hypothetical example, imagining a team working on a commercial software product when a bug is introduced:
Testability is a key concern when following TDD practices. There are many different ways to accidentally write difficult-to-test code, but fortunately they tend to fall into fairly broad categories. In this post, we’ll examine some common coding pitfalls that introduce testing challenges, and future installments will discuss ways to work around or avoid these issues.
So What Do You Mean By Testability?
Testability is a measure of how easy (or difficult) it is to write tests for a given class. There are several development practices and patterns that work together to improve a given class’ testability, including Dependency Injection and SOLID Principles. In general, highly-testable classes will have several traits in common:
- Dependencies are specified as abstractions (either interfaces or abstract classes)
- Dependencies are injected rather than created internally
- Factories are used to create objects with runtime dependencies
- Avoidance of static classes and methods wherever possible
So let’s take a quick look at a class that is inherently difficult to test and identify the various problems…
Over the years I have introduced TDD practices to multiple teams, both as an engineer and as a consultant. Often I find that teams have some awareness of TDD process and tools, but have fundamental misunderstandings about the process and its benefits. The initial perception that I generally see is that TDD adds work to the development cycle while adding minimal benefit. In fact, although adopting TDD requires some time investment, the final benefits far outweigh the initial pain points.
In a way, the process of adopting TDD is very similar to committing to a fitness goal:
So now we come to the true confessions part of the story…
You calmly lean back in your chair for a moment, taking in the news. “Wow, this is a pretty big change at this point in the release cycle, but I’m sure Management has considered those risks and the potential for a negative impact. They’ve committed to this course of action?” “Yes, and they’re not willing to make any compromises at all – this is a mandate.” says the PM.
For a brief moment, your mind flashes back to the meetings from last year when the team discussed development process, coding standards, and other related practices, unanimously voting to adopt TDD. You also recall standing your ground in a few conversations with upper management, helping them understand the value of TDD despite their initial impression that it was just busywork. Your mind then leaps to memories of the initial implementation, how the team had to expend additional effort building test infrastructure, shift their mindset to consider testability, and fully commit to architectural standards. At first, you recall, the learning curve was brutal but that effort paid rich dividends as the project matured, requiring less and less testing infrastructure while still providing appropriate code coverage. “And now,” you muse to yourself, “we’ll get to demonstrate to business and management how TDD also mitigates risk from unexpected changes.”
Although outwardly calm, internally you break into a cold sweat, thinking of all the changes they’ve just dropped into your lap. Your gut clenches, thinking about the explosion of possible side effects those changes will have. Since this is a non-TDD environment, regressions can only be detected manually, adding days to the QA testing schedule. As a slight silver lining, you do have some tests, but they’re a combination of low-value unit tests and slow-running integration tests, so they will be generally unhelpful in this endeavor.
The more pressing issue, however, is that the existing codebase was written sloppily. The team ended up leveraging many shortcuts like tightly-coupled classes and lack of dependency injection. Because of this coupling, removing the feature will have far-ranging effects on the codebase, putting the release at risk.