Appendix A. Design and testability

Changing the design of your code so that it’s more easily testable is a controversial issue for some developers. This appendix will cover the basic concepts and techniques for designing for testability. We’ll also look at the pros and cons of doing so and when it’s appropriate.

First, though, let’s consider why you would need to design for testability in the first place.

A.1. Why should I care about testability in my design?

The question is a legitimate one. In designing our software, we’re taught to think about what the software should accomplish, and what the results will be for the end user of the system. But tests against our software are yet another type of user. That user has strict demands for our software, but they all stem from one mechanical request: testability. That request can influence the design of our software in various ways, mostly for the better.

In a testable design, each logical piece of code (loops, ifs, switches, and so on) should be easy and quick to write a unit test against, one that demonstrates these properties:

  • Runs fast
  • Is isolated, meaning it can run independently or as part of a group of tests, and can run before or after any other test
  • Requires no external configuration
  • Provides a consistent pass/fail result

These are the FICC properties: fast, isolated, configuration-free, and consistent. If it’s hard to write such a test, or it takes a long time to write it, the system isn’t testable.

If you think of tests as a user of your system, designing for testability becomes a way of thinking. If you were doing test-driven development, you’d have no choice but to write a testable system, because in TDD the tests come first and largely determine the API design of the system, forcing it to be something that the tests can work with.

Now that you know what a testable design is, let’s look at what it entails, go over the pros and cons of such design decisions, and discuss alternatives to the testable design approach.

A.2. Design goals for testability

There are several design points that make code much more testable. Robert C. Martin has a nice list of design goals for object-oriented systems that largely form the basis for the designs shown in this chapter. See his “Principles of OOD” article at http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod.

Most of the advice I include here is about allowing your code to have seams—places where you can inject other code or replace behavior without changing the original class. (Seams are often talked about in connection with the Open Closed Principle, which is mentioned in the Martin’s “Principles of OOD” article.) For example, in a method that calls a web service, the web service API can hide behind a web service interface, allowing us to replace the real web service with a stub that will return whatever values we want, or with a mock object. Chapters 35 discuss fakes, mocks, and stubs in detail.

Table A.1 lists some basic design guidelines and their benefits. The following sections will discuss them in more detail.

Table A.1. Test design guidelines and benefits

Design guideline

Benefit(s)

Make methods virtual by default.

This allows you to override the methods in a derived class for testing. Overriding allows for changing behavior or breaking a call to an external dependency.

Use interface-based designs.

This allows you to use polymorphism to replace dependencies in the system with your own stubs or mocks.

Make classes nonsealed by default.

You can’t override anything virtual if the class is sealed (final in Java).

Avoid instantiating concrete classes inside methods with logic. Get instances of classes from helper methods, factories, Inversion of Control containers such as Unity, or other places, but don’t directly create them.

This allows you to serve up your own fake instances of classes to methods that require them, instead of being tied down to working with an internal production instance of a class.

Avoid direct calls to static methods. Prefer calls to instance methods that later call statics.

This allows you to break calls to static methods by overriding instance methods. (You won’t be able to override static methods.)

Avoid constructors and static constructors that do logic.

Overriding constructors is difficult to implement. Keeping constructors simple will simplify the job of inheriting from a class in your tests.

Separate singleton logic from singleton holder.

If you have a singleton, have a way to replace its instance so you can inject a stub singleton or reset it.

A.2.1. Make methods virtual by default

Java makes methods virtual by default, but .NET developers aren’t so lucky. In .NET, to be able to replace a method’s behavior, you need to explicitly set it as virtual so you can override it in a default class. If you do this, you can use the Extract and Override method that I discussed in chapter 3.

An alternative to this method is to have the class invoke a custom delegate. You can replace this delegate from the outside by setting a property or sending in a parameter to a constructor or method. This isn’t a typical approach, but some system designers find this approach suitable. Listing A.1 shows an example of a class with a delegate that can be replaced by a test.

Listing A.1. A class that invokes a delegate that can be replaced by a test
public class MyOverridableClass
{
public Func<int,int> calculateMethod=delegate(int i)
{
return i*2;
};
public void DoSomeAction(int input)
{
int result = calculateMethod(input);
if (result==-1)
{
throw new Exception("input was invalid");
}
//do some other work
}
}

[Test]
[ExpectedException(typeof(Exception))]
public void DoSomething_GivenInvalidInput_ThrowsException()
{
MyOverridableClass c = new MyOverridableClass();
int SOME_NUMBER=1;

//stub the calculation method to return "invalid"
c.calculateMethod = delegate(int i) { return -1; };

c.DoSomeAction(SOME_NUMBER);
}

Using virtual methods is handy, but interface-based designs are also a good choice, as the next section explains.

A.2.2. Use interface-based designs

Identifying “roles” in the application and abstracting them under interfaces is an important part of the design process. An abstract class shouldn’t call concrete classes, and concrete classes shouldn’t call concrete classes either, unless they’re data objects (objects holding data, with no behavior). This allows you to have multiple seams in the application where you could intervene and provide your own implementation.

For examples of interface-based replacements, see chapters 35.

A.2.3. Make classes nonsealed by default

Some people have a hard time making classes nonsealed by default because they like to have full control over who inherits from what in the application. The problem is that, if you can’t inherit from a class, you can’t override any virtual methods in it.

Sometimes you can’t follow this rule because of security concerns, but following it should be the default, not the exception.

A.2.4. Avoid instantiating concrete classes inside methods with logic

It can be tricky to avoid instantiating concrete classes inside methods that contain logic because we’re so used to doing it. The reason for doing so is that later our tests might need to control what instance is used in the class under test. If there’s no seam that returns that instance, the task would be much more difficult unless you employ external tools, such as Typemock Isolator. If your method relies on a logger, for example, don’t instantiate the logger inside the method. Get it from a simple factory method, and make that factory method virtual so that you can override it later and control what logger your method works against. Or use constructor injection instead of a virtual method. These and more injection methods are discussed in chapter 3.

A.2.5. Avoid direct calls to static methods

Try to abstract any direct dependencies that would be hard to replace at runtime. In most cases, replacing a static method’s behavior is difficult or cumbersome in a static language like VB.NET or C#. Abstracting a static method away using the Extract and Override refactoring (shown in section 3.4.6 of chapter 3) is one way to deal with these situations.

A more extreme approach is to avoid using any static methods whatsoever. That way, every piece of logic is part of an instance of a class that makes that piece of logic more easily replaceable. Lack of replaceability is one of the reasons some people who do unit testing or TDD dislike singletons; they act as a public shared resource that is static, and it’s hard to override them.

Avoiding static methods altogether may be too difficult, but trying to minimize the number of singletons or static methods in your application will make things easier for you while testing.

A.2.6. Avoid constructors and static constructors that do logic

Things like configuration-based classes are often made static classes or singletons because so many parts of the application use them. That makes them hard to replace during a test. One way to solve this problem is to use some form of Inversion of Control containers (such as Microsoft Unity, Autofac, Ninject, StructureMap, Spring.NET, or Castle Windsor—all open source frameworks for .NET).

These containers can do many things, but they all provide a common smart factory, of sorts, that allows you to get instances of objects without knowing whether the instance is a singleton, or what the underlying implementation of that instance is. You ask for an interface (usually in the constructor), and an object that matches that type will be provided for you automatically, as your class is being created.

When you use an IoC container (also known as a dependency injection container), you abstract away the lifetime management of an object type and make it easier to create an object model that’s largely based on interfaces, because all the dependencies in a class are automatically filled up for you.

Discussing containers is outside the scope of this book, but you can find a comprehensive list and some starting points in the “List of .NET Dependency Injection Containers (IOC)” article on Scott Hanselman’s blog: http://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC.aspx.

A.2.7. Separate singletons and singleton holders

If you’re planning to use a singleton in your design, separate the logic of the singleton class and the logic that makes it a singleton (the part that initializes a static variable, for example) into two separate classes. That way, you can keep the single responsibility principle (SRP) and also have a way to override singleton logic.

For example, listing A.2 shows a singleton class, and listing A.3 shows it refactored into a more testable design.

Listing A.2. An untestable singleton design
public class MySingleton
{
private static MySingleton _instance;
public static MySingleton Instance
{
get
{
if (_instance == null)
{
_instance = new MySingleton();
}

return _instance;
}
}
}
Listing A.3. The singleton class refactored into a testable design

Now that we’ve gone over some possible techniques for achieving testable designs, let’s get back to the larger picture. Should you do it at all, and are there any consequences of doing it?

A.3. Pros and cons of designing for testability

Designing for testability is a loaded subject for many people. Some believe that testability should be one of the default traits of designs, and others believe that designs shouldn’t “suffer” just because someone will need to test them.

The thing to realize is that testability isn’t an end goal in itself, but is merely a byproduct of a specific school of design that uses the more testable object-oriented principles laid out by Robert C. Martin (mentioned at the beginning of section A.2). In a design that favors class extensibility and abstractions, it’s easy to find seams for test-related actions. All the techniques shown in this appendix so far are very much aligned with Robert Martin’s principles.

The question remains, is this the best way to do things? What are the cons of such a method? What happens when you have legacy code? And so on.

A.3.1. Amount of work

In most cases, it takes more work to design for testability than not because doing so usually means writing more code.

You could argue that the extra design work required for testability points out design issues you hadn’t considered and that you might have been expected to incorporate in your design anyway (separation of concerns, single responsibility principle, and so on).

On the other hand, assuming you’re happy with your design as is, it can be problematic to make changes for testability, which isn’t part of production. Again, you could argue that test code is as important as production code, because it exposes the API usage characteristics of your domain model and forces you to look at how someone will use your code.

From this point on, discussions of this matter are rarely productive. Let’s just say that more code, and work, is required when testability is involved, but that designing for testability makes you think about the user of your API more, which is a good thing.

A.3.2. Complexity

Designing for testability can sometimes feel a little (or a lot) like it’s overcomplicating things. You can find yourself adding interfaces where it doesn’t feel natural to use interfaces, or exposing class behavior semantics that you hadn’t considered before. In particular, when many things have interfaces and are abstracted away, navigating the code base to find the real implementation of a method can become more difficult and annoying.

You could argue that using a tool such as ReSharper makes this argument obsolete, because navigation with ReSharper is much easier. I agree that it eases most of the navigational pains. The right tool for the right job can help a lot.

A.3.3. Exposing sensitive IP

Many projects have sensitive intellectual property that shouldn’t be exposed, but which designing for testability would force to be exposed: security or licensing information, or perhaps algorithms under patent. There are workarounds for this—keeping things internal and using the [InternalsVisibleTo] attribute—but they essentially defy the whole notion of testability in the design. You’re changing the design but still keeping the logic hidden. Big deal.

This is where designing for testability starts to melt down a bit. Sometimes you can’t work around security or patent issues. You have to change what you do or compromise on the way you do it.

A.3.4. Sometimes you can’t

Sometimes there are political or other reasons for the design to be done a specific way, and you can’t change or refactor it. Sometimes you don’t have the time to refactor your design, or the design is too fragile to refactor. This is another case where designing for testability breaks down—when you can’t or won’t do it.

Now that we’ve gone through some pros and cons, it’s time to consider some alternatives to designing for testability.

A.4. Alternatives to designing for testability

It’s interesting to look outside the box at other languages to see other ways of working.

In dynamic languages such as Ruby or Smalltalk, the code is inherently testable because you can replace anything and everything dynamically at runtime. In such a language, you can design the way you want without having to worry about testability. You don’t need an interface in order to replace something, and you don’t need to make something public to override it. You can even change the behavior of core types dynamically, and no one will yell at you or tell you that you can’t compile.

In a world where everything is testable, do you still design for testability? The answer is, of course, “no.” In that sort of world, you’re free to choose your own design.

Consider a .NET-related analogy that shows how using tools can change the way you think about problems, and sometimes make big problems a non-issue. In a world where memory is managed for you, do you still design for memory management? Mostly, “no” would be the answer. People working in languages where memory isn’t managed for them (C++, for example) need to worry about and design for memory optimization and collection, or the application will suffer.

In the same way, by following testable object-oriented design principles, you might get testable designs as a byproduct, but testability should not be a goal in your design. It’s just there to solve a specific problem. If a tool comes along that solves the testability problem for you, there will be no need to design specifically for testability. There are other merits to such designs, but using them should be a choice and not a fact of life.

The main problems with nontestable designs is their inability to replace dependencies at runtime. That’s why we need to create interfaces, make methods virtual, and do many other related things. There are tools that can help replace dependencies in .NET code without needing to refactor it for testability. One such tool is Typemock Isolator (www.Typemock.com), a commercial tool with an open source alternative.

Does the fact that a tool like Isolator exists mean we don’t need to design for testability? In a way, yes. It rids us of the need to think of testability as a design goal. There are great things about the OO patterns Bob Martin presents, and they should be used not because of testability, but because they seem right in a design sense. They can make code easier to maintain, easier to read, and easier to develop, even if testability is no longer an issue.

A.5. Summary

In this appendix, we looked at the idea of designing for testability: what it involves in terms of design techniques, its pros and cons, and alternatives to doing it. There are no easy answers, but the questions are interesting. The future of unit testing will depend on how people approach such issues, and on what tools are available as alternatives.

Testable designs usually only matter in static languages, such as C# or VB.NET, where testability depends on proactive design choices that allow things to be replaced. Designing for testability matters less in more dynamic languages, where things are much more testable by default. In such languages, most things are easily replaceable, regardless of the project design.

Testable designs have virtual methods, nonsealed classes, interfaces, and a clear separation of concerns. They have fewer static classes and methods, and many more instances of logic classes. In fact, testable designs are what SOLID design principles have stood for. Perhaps it’s time that the end goal should not be testability, but good design instead.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.168.163