开发者

TDD and encapsulation priority conflict

I just started practicing TDD in my projects. I'm developing a project now using php/zend/mysql and phpunit/dbunit for testing. I'm just a bit distracted on the idea of encapsulation and the test driven approach. My idea behind encapsulation is to hide access to several object functionalities. To make it more clear, private and protected functions are not directly testable(unless you will create a public function to call it).

So I end up converting some private and protected functions 开发者_运维百科to public functions just to be able to test them. I'm really violating the principles of encapsulation to give way to micro function testability. Is this the correct way of doing it?


There is a pretty standard answer to this in TDD circles. If there is functionality in a class that you both want hidden and directly tested, you should sprout a class with that functionality. This is a great example of how TDD improves your design.

In the original class, that extraneous functionality is gone, wrapped within the sprouted class, so the original class' design is simpler, and better conforms to the Single Responsibility Principle. In the sprouted class, the extracted functionality is its raison d'etre, therefore it's appropriate for it to be public, and therefore it's testable without test-only modifications.


With respect there Carl Manaster's fine answer, there are some drawbacks you should at least consider before embarking on the path Carl suggested.

The most significant of which is this: we use encapsulation to minimise the number of potential dependencies that carry the greatest probability of change propagation. In your case, you have encapsulated private methods inside your class: they are not available to other classes and thus there are no potential dependencies on them: the cost of any changes you make to them is minimised and has a low probablility of propagation to other classes.

It seems that Carl suggests moving some private methods from your class into a new class, and making those methods public (so that you can test them). (Incidentally, why not just make them public in the original class?)

By doing this, you remove the barrier to other classes' forming dependencies on those methods, which will potentially increase the cost of chaging those methods should any other class take to using them.

You may judge this down-side minor and a worthwhile price to pay for being able to test your private methods, but at least be aware of it. In a small number of cases, it may indeed be worthwhile, but if you institute this throughout your code-base then you'll drastically increase the probability that these dependencies will form, increasing the cost of your maintenance cycle to an unknown degree.

For these reasons, I disagree with Carl that his suggestion is, ” … a great example of how TDD improves your design.”

Furthermore, he states, ”In the original class, that extraneous functionality is gone, wrapped within the sprouted class, so the original class' design is simpler, and better conforms to the Single Responsibility Principle.”

I would argue that the functionality being moved is not at all, ”Extraneous.” Also, ”Simpler,” is a not well-defined: it certainly may be the case that a class's simplicity is inversely proportional to its size but that does not mean that a system of simplest-possible classes will be the simplest possible system: if this were the case, all classes would contain only one method and a system would have an enormous number of classes; the removal of this hierarchical layer of multiple-methods-within-classes, it could be argued, would make the system much more complicated.

The Single Responsibility Principle (SRP) is, furthermore, notoriously subjective and entirely dependent on the level of abstraction of the observer. It is not at all the case that removing a method from a class automatically improves its conformity to the SRP. A Printer class, with 10 methods, has the single responsibility of printing at the level of abstraction of the class. One of its methods may be checkPrinterConnected() and one may be checkPaper(); at the method level, these are clearly separate responsibilities, but they do not automatically suggest that the class should be broken down into further classes.

Carl finishes, ”In the sprouted class, the extracted functionality is its raison d'etre, therefore it's appropriate for it to be public, and therefore it's testable without test-only modifications.” A functionality's importance (it's raison-d'etre-ness) is not the basis for the appropriateness of its being public. The basis for the appropriateness of functionality's being public is the minimising of the interface exposed to the client such that the class's functionality is available for use while the client's independence of the functionality's implementation is maximised. Of course, if you are moving just one method into the sprouted class, then it has to be public. If you are moving more than one method, however, you must make those methods public which are essential to the client's successful use of the class: these public methods may be far less important than some of the private methods from which you wish to shield your client. (In any case, I'm not at a fan of this, ”Raison-d'etre,” phrase as the importance of a method is also not well-defined.)

An alternative approach to Carl's suggest depends on how large you envisage your system to grow. If it will grow to fewer than a few thousand classes, then you might consider having a script to copy your source code to a new directory, change all occurances of, ”private” to, ”public” in that copied source and then write your tests against the copied source. This has the down-side of the time it takes to copy the code but the benefit of preserving encapsulation your original source yet making all the methods testable in the copied version.

Below is the script I use for this purpose.

Regards,

Ed Kirwan

!/bin/bash

rm -rf code-copy

echo Creating code-copy ...

mkdir code-copy

cp -r ../www code-copy/

for i in find code-copy -name "*php" -follow; do

sed -i 's/private/public/g' $i

done

php run_tests.php


I have just read a great article on letting mock objects drive you design:

http://www.mockobjects.com/files/usingmocksandtests.pdf

When Carl says "you should sprout a class with that functionality", the author of this article explain how your tests can guide you, through the use of mock objects, how you can design your class so you 1) don't need to worry about not being able to test private parts, and more importantly 2) how this will improve your design by (I'll paraphrase Carls quote) discovering collaborators and roles with the right responsibility.

The author takes you through an example step by step to make his point very clear.

Here's another article with the same approach:

http://www.methodsandtools.com/archive/archive.php?id=90

A quote:

Many who start with TDD struggle with getting a grip on dependencies. To test an object, you exercise some behaviour and then verify whether an object is in an expected state. Because OO design focuses on behaviour, the state of an object is usually hidden (encapsulated). To be able to verify if an object behaves like expected, you sometimes need to access the internal state and introduce special methods to expose this state, like a getter method or a property that retrieves the internal state.

Apart from not wanting objects cluttering their interfaces and exposing their private parts, we neither want to introduce unnecessary dependencies with such extra getters. Our tests will become too tightly coupled and focused on implementation details.

A group of agile software development pioneers from the United Kingdom were also struggling with this back in 1999. They had to add additional getter methods to validate the state of objects. Their manager didn't like all this breaking of encapsulation and declared: I want no getters in the code! (Mackinnon et al., 2000 & Freeman et al., 2004)

The team came up with the idea to focus on interactions rather than state. They created a special object to replace the collaborators of objects under test. These special objects contained specifications for expected method calls. They called these objects mock objects, or mocks for short. The original ideas have been refined, resulting in several mock object frameworks for all common programming languages: Java (jMock, EasyMock, Mockito), .NET (NMock, RhinoMocks), Python (PythonMock, Mock.py, Ruby (Mocha, RSpec), C++ (mockpp, amop). See www.mockobjects.com for more information and links.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜