Design and Testability
Design Dilemmas in the Real World
Blog: ISerializable.com (will have slides there)
- Dilemmas that he’s faced, we’re probably starting to face, Microsoft is facing
- Many have to do with change
- Design has changed very little in the last few years, but maybe it’s time for a new thought, or some new compromises, or lack of compromises
Has been on two sides of the same coin
- Pure design, dependency injection, hardcore freaks
- Being pragmatic
- What you can do about testability when tooling is not available to help you (or you choose not to use it)
- Benefits of testable designs
- Why should I care about testability?
- Defining a testable system
- Examples of implementing and refactoring testable code
- Extract interface, virtual methods…
- Dependency injection and IoC containers
- Legacy code, and what happens when you can’t change your design
- Silver bullets
- Why current systems make this hard
Me Me Me
- The Art of Unit Testing
- Next generation isolation platform (Isolator)
- Next generation testing tools (shhhh…)
- Some things are changed, some are a little more powerful, some things feel a little weird
Before we start
- This is just one way to do design
- Assuming you have experience with NUnit or MS Test
Why should I care?
- Framework authors
- Maintainable system. Testing is about quality, and a security net that lets you know when you screw up.
- Extensible system, like LEGOs.
- Code quality
Automated unit/acceptance testing is important
- Helps with regression testing
- Maintainable system
- Finds bugs early
- Code quality
- Better understanding of requirements
- Most people write code from the inside out: write the method, without caring about who’s going to be calling it
- The test is using the API, so you have to think about requirements, design, usability
- Also means it makes your coding take longer… but code quality and maintenance are better, and release cycle is actually shorter
- API documentation
- “Executable” requirements. It’s good for the customers, because they can run actual tests.
- Big database change — not a big problem.
- Even bad tests can help more than you think.
Unit level testing requires testability
- How easily can you…
- Create a new instance? Do you need to create a configuration file first? Configure a third-party library?
- Get a consistent result? Do methods have side effects? Talk to a database or Web service?
- Test just X without Y?
- Control inputs and outputs from X?
- Isolate X from its dependencies?
Two kinds of testability
- Testable application
- Test enabling framework
- ASP.NET MVC, WebForms. WebForms code depends on the WebForms framework, which is not test enabled. Much easier to test code built on ASP.NET MVC.
- SharePoint. Untestable by default: lots of statics, global objects. Testing your logic is almost impossible.
- MS CRM. Even more horrible than SharePoint.
- WCF, WF. Not test enablers by default. WF has lots of sealed classes, multiple processes, multiple threads, no places to intervene. Lots has to do with security, but lots has to do with the way things are done.
- If you’re a framework author, making your framework test-enabled should be one of your main concerns.
What is a unit-testable system?
- Lets us write good tests easily
- For each piece of coded logic, a unit test can be written easily enough to verify it works as expected, while keeping the PC-COF rules
- Partial runs are possible
- Configuration is not needed
- Consistent pass/fail result
- Order does not matter
- Fast (what is “fast”? We’ll talk about that)
Consistent pass/fail result
- Can you trust your test?
- Control of dependencies (you want to test a method, but it starts with a call to a static method e.g. to validate)
- Design: Extract interface for Validator
- Test: send in a “fake” validator
- He implements with a public field saying what IsValid should return
- Inject the IPersonValidator via e.g. a constructor parameter
- OO purists are panicking that you’re letting someone else decide how to do a core part of your functionality
Side note: if you write a test, give it a good name. Don’t call it Test1, Test2, Test3. He will personally choke you.
- His name suggested naming convention:
- Don’t name it starting with “Test”, because the attribute already tells you it’s a test
This works, but it kind of sucks. You end up with lots of interfaces, and your classes have lots of dependencies.
- Constructor injection
- Use for dependencies that are not optional.
- Property injection
- That means it’s an optional dependency.
- Factory injection (especially if you already have a factory)
- Virtual method injection
The dependency problem
- People just want to use the code that you wrote. They don’t care about passing in a logger and a validator.
- To create a logger, you need an exception handler and a data writer.
Inversion of Control containers
- Kind of like really smart factories. You ask them for a new instance of some type, they’ll look at the constructor’s dependencies, and if you’ve configured it correctly, it’ll know which objects to pass for those dependencies. If those also have dependencies, it handles it recursively.
- StructureMap (not officially released lately, must build from source)
- Microsoft Unity Application Block — Unlike many other Application Blocks from Microsoft, it doesn’t suck.
- Very configurable, from XML or from code
Demos: Windsor and Unity
- Create a WindsorContainer
- Add components — generic syntax. Either give it a class, or an interface and a class.
- Have to also add the type you’re going to resolve (instantiate).
() — constructs something.
- Some people believe your tests should call the constructor directly with your fake dependencies, instead of using containers from tests.
- Create a UnityContainer
- Call RegisterType
- Fluent interface — you can chain calls to RegisterType.
- Don’t have to register the class you’re going to resolve (instantiate).
- Tries to use the constructor with the most parameters. Gives an exception if the container doesn’t know how to create one of thoes parameters.
Important: Need to make sure everyone uses container.Resolve instead of new.
Fast run times
- A test that takes one second is not fast.
- Want someone to be able to get latest version, right-click, and run all the tests. It should be that simple. And all the tests should pass.
- If they have to wait a long time, they won’t run all the tests.
No configuration needed
- How easy is it to get latest and run the tests?
- If a config file is needed, how do you test a bad connection string? It quickly becomes not easy and not fast to write that sort of test.
Solving configuration with override
- Works if you don’t want to add interfaces
- Extract dependency into a virtual method
- This virtual method needs to be the stupidest code you’ve ever written. Absolutely trivial.
- Class can’t be sealed, so you can’t do this with WCF, WF, SharePoint, etc.
- Easier; pollutes the code less; but it forces you to make a lot of derived classes in your tests
- Good for simple scenarios, maybe not when you get more complexity
Test order should not matter for consistent results
- Some people don’t mind that tests share data
- If tests share data, you have dependencies between tests
- That’s BAD, because you’ll kill yourself when you need to maintain tests. You remove a test and something fails. You modify a test and something else fails.
- Debugging test dependencies is awful. You can spend days on this.
- If tests use a shared resource (e.g. a database), you need to fix the tests, not the application.
- Need to restore shared state in
TearDown. For databases, create a
Partial runs of tests should produce consistent results
- Sometimes you want to run only some of the tests. Sometimes tests do take a long time; that’s the real world.
- Some test frameworks let you re-run just the failed tests.
Are singletons evil?
.Instance probably is. Getting it from an IoC container, maybe not so much.
- How do you instantiate it for testing? (And re-instantiate it, e.g. if you modify the configuration that it loads on startup)
- Shared resource between tests
- Single Responsibility.
- There’s a class being held, and the one doing the holding and making sure there’s one instance.
- Can refactor into testable ones: separate holder to a separate class, or refactor using a container.
- Unity: RegisterType
- Global variables
- Tight coupling
- State that lasts forever
The GOD Method
- You know it’s important; you don’t know why it does what it does; sometimes it’s cruel to you
- One huge do-it-all method
- Prevents maintenance, impossible to test
- Can’t refactor for testability, because you might introduce bugs. Solution: integration-level tests.
- Avoid this by design
- Keep single responsibility principle
- Calls to little methods
- Integration testing
- Refactor only as needed. It works. Don’t touch it.
- Typemock Isolator (typemock.com)
- Feathers: “Working Effectively with Legacy Code“
Test Driven Development/Design
- Tests have to ues a testable class
- Test-first means testable-design-first
- Test can later show you how you’d like the API to be used
- Decoupled design and architecture
- More control on state simulations
- Avoid BDUF (Big Design Up Front). Tends to result in highly untestable code. Do EDUF (Enough Design Up Front) instead, but your design should be able to change as you go.
- Interface-based designs. Tend to be easier to understand in terms of roles. A Logger is a role.
- Try to avoid Singletons
- IoC containers
- Avoid GOD methods — use polymorphism instead
- Virtual by default
- new X() -> Factory.MakeX(). When you abstract away the creation, you can replace the creation.
- Single Responsibility for class and method. If our class or method is elaborate and does more than one thing, it’s usually a design smell.
- Design for testability leads to agility
- You need tests when someone changes the requirements
- Common patterns
- Recognizing test challenges in the design
- Refactoring the design for testability
- Test-driven development leads to a testable design
- Testable code == well designed, decoupled design