After reading Roy’s Post and Oren’s Post who strongly side on Designing for testability I suddenly felt a De Ja Vu.
De Ja Vu
A few years ago, I was managing a project in a big company. One of the features that we where developing was a caching system. We needed the caching to boost performance on a multi location, multi server application.
We developed in a normal Waterfall methodology, and the architect (a really funny and talented guy) came up after a few weeks with his design.
The design was really well thought out and the cache was ready to support – multiple databases with many different kinds of databases along with other things. At the time we believed that we had to design the system with the future in mind.
The question did arise:
Do we need to support Multiple Database Instances?
The Answer was, No, Not really, but if we are going to support it later then the redesign will take us ages.
When I asked the team to read and tell me what they think about: “You Ain’t Gonna Need It“, everyone took this personally. The architect started to feel that his job has been taken away from him:
“I am getting paid to think about future problems” and the developers where also taken aback. They have all been trained to create flexible systems and not flexible processes.
I failed to convince them, and a couple of years later, the caching system became what I call “a black hole”. No one knew exactly how it works, and everyone was scared to change it, because it was soo complex. The developers now said: “We didn’t really need multiple database instance, we spent ages designing and developing this feature, and every bug we find, has to support this feature too, which just complicates matters, because no one uses it but we cannot remove it.”
Still when I talk to them about YAGNI, they all say: “But when we thought ahead and designing the <fill in whatever feature>, we saved weeks and could implement <another featrure> in days. It would have taken us weeks without it”.
I have to remind them that it took months to design, implement and test before we even needed it.
YAGNI, Low Coupling and Testability
So, what has this got to do with Oren and Roy’s posts.
Well, I am getting the same feeling from them that I got from my architect.
Oren, so you found that a “Couple of weeks after introducing the IFileWriterFactory” you found another use for it, and it saved you time.
Good for you!
But this is NOT YAGNI, the opposite. It sounds just like that <fill in whatever feature> that my team was so proud that it had managed to predict.
What about all the other features that have no use? The price of doing this everywhere can be quite expensive. Most of the time (apart from tests) – You Just Ain’t Gonna Need It.
There was a time when we had no choice, you had to either Design for Testability or not test at all. But in these times this is no longer true.
So, Does testing – driving your design, actually create the best design? I am not sure.
It does one thing for sure, It creates a low coupled (modular) design. But low coupling is NOT a silver bullet. A good design has to take and BALANCE many factors, this is a complex task. Adding Testability to these factors might unbalance your design.
Quiz: Which is easier to maintain:
File.WriteAllText(filename, text);
or
using(TextWriter writer = IoC.Resolve<IFileWriterFactory>().Create(filename)) writer.Write(text);
Failing to take the bait, or perhaps that’s what I just did 🙂
I have no answer to the quiz. How can I know that the second code snippet is not the implementation of the first?
Joking aside, could it be we disagree because we have different understanding of what a testable design is? I have seen a lot of arguments flying around, but no code. No design.
Then I got this feeling that you design frameworks, while I design applications. Different worlds, different rules.
Maybe it’s time to discuss actual code?
Thomas,
Isn’t the code above actual code?
Imagine that you are maintaining an application and you see the two lines above.
Which one would you understand quicker, find easier to comprehend.
I don’t think that it is a difference between designing frameworks and applications. Although if you need to expose your API’s then it is even more important not to expose API’s that are created for the sake of testability.
But just being easier to understand makes the code a lot easier to maintain. And we all know that 80% of the developers work is in maintenance.
Balancing Maintainability, Readability and Testability
How about:
public void DoSomething(IFileSystem fileSystem)
{
fileSystem.WriteAllText(filename, text);
}
I thought a lot about your YAGNI argument against using polymorphism to insert mock objects in tests. I think you are forgetting that unit tests are a part of the system’s code and that their readability and maintainability is just as important as that of the rest of the system. This is why inserting access points in my design for tests is not a YAGNI – I need it for the tests.
Your argument is that I don’t have to use polimorphism because TypeMock can replace concrete calls at runtime – but that is true not only for unit tests. Using reflection I can replace concrete calls at runtime wherever I wish but this is obviously a poor design decision. So why are tests any different?
Now let me ask you what is easier to maintain, this:
public void DoSomething()
{
File.WriteAllText(filename, text);
}
[Test]
public void TestWithTypeMock()
{
MockManager.Init();
Mock myMock = MockManager.Mock(typeof(File));
myMock.ExpectCall(“WriteAllText”);
DoSomething();
myMock.Verify();
}
or this:
public void DoSomething(IFile file)
{
file.WriteAllText(filename,text);
}
class MockFile : IFile
{
public bool _writeAllTextCalled = false;
public void WriteAllText(string filename, string text)
{
_writeAllTextCalled = true;
}
}
[Test]
public void TestUsingStandartMock()
{
MockFile file = new MockFile();
DoSomething(file);
NUnit.Framework.Assert.IsTrue(file._writeAllTextCalled);
}
Which one of these implementations would better survive a change in the name “WriteAllText”?
Yoni,
Thanks for your thoughts, they are interesting and you have a point here.
I have had this discussion over 4 years ago.
At that time it was a hot debate on the net.
OO and XP purist where debating whether refactoring code for testability was violating YAGNI.
The purist where saying:
“refactoring your code just to make it testable does NOT violate YAGNI because you need it “right now” for testing”
I will add: And Testing has business value.
But today: You don’t need to change your code to make it testable, so the whole issue is resolved.
About your code example: The way YAGNI works is that you start with the first example.
If and only if there is a new feature that we have to add, we refactor our code (extract and introduce a new method and implement the new feature – Some IDE’s will even find similar lines and do all the hard stuff for you).
So if in some time in the future you have to send all the messages to an SMS for example, then you can refactor your code. And by the way, in most cases you won’t have to change your tests, because you will leave the older implementation.
Also notice that in the second example, now all code that calls DoSomething() must know IFile. So you are actually lowering the cohesion (by having a less encapsulated system) in order to have a lower coupled system. This is a Design Decision that ‘Testabilty’ automatically makes for you.
“But today: You don’t need to change your code to make it testable, so the whole issue is resolved.”
In that case, I also don’t need to refactor in order to support writing to an SMS. All I have to do is run my application on a TypeMock-like framework and have the File class be substituted at runtime to a different implementation (one that sends SMS’s) before calling DoSomething().
Would you like maintaining code that hooks on class instantiators and replaces them at runtime instead of using plain old polymorphism?
But that it exactly what TypeMock does so you are saying that a technique no-one would want to use in production code is good for test code – why?
You are right, of course, about my DoSomething() that accepts IFile. But if you have a test that asserts that the File class is being used inside DoSomething() you have the same problem with encapsulation because once a different class is used to perform the same behaviour your test will have to be rewritten.
Yoni,
Welcome to Aspect Oriented Programming/Aspect Oriented Software Design.
This is exactly what an AOD Framework can do and more. And yes it is used in production code 🙂
Adding a testability aspect without changing your code is only one of many aspects you can add.
Others can be:
* security
* performance
* optimization
* accuracy
* data representation
* data flow
* portability
* traceability
* profiling
(See http://www.codeproject.com/gen/design/aop.asp)
And with the right tools, this can and will be easy.
🙂 Nice.
Well, my bad, I thought we were discussing OOP. AOP is not exactly my cup of tea as you can tell. But that’s a whole new discussion…
Using AOP and wiring class initializers instead of simple polymorphism, seems to me like a You Ain’t Gonna Need It 🙂
Yonni,
Anders found a really funny post on the subject: http://andersnoras.com/blogs/anoras/archive/2007/03/06/found-the-day-i-tried-to-learn-spring-and-hibernate.aspx
It is a good one indeed. It has little to do with designing for testability though.
You see, using a container that loads different implementations at runtime based on a configuration file is just as awkward as hooking to constructors at runtime and returning a different implementation. The obvious way of doing things is simply having the tested code use an external object you can easily replace at runtime by simply sending in a mock that exposes the same interface.
But I guess I’m just making myself more enemies now so maybe it’s time to move on to a different subject.
I have been thinking hard if I should actually reply to the last post as it will be explaining a joke, and, well, explaining jokes is an useless as searching Google without an internet connection.
[If you didn’t understand the context. The post is about a (well known) IoC implementation.]