开发者

How do I factor code to ease testability?

开发者 https://www.devze.com 2022-12-12 18:51 出处:网络
I am learning about Unit Testing and want to know how to write testable code. But, I\'m not sure how to write testable code without making it complex. I\'ll take famous Car and Engine problem to descr

I am learning about Unit Testing and want to know how to write testable code. But, I'm not sure how to write testable code without making it complex. I'll take famous Car and Engine problem to describe the problem.

class Car
{
private:
   Engine m_engine;

public:
   Car();
   // Rest of the car
}

I came up with following solutions to make the above code testable.

  1. Changing the Car's constructor to take Engine as a parameter. Then mock the Engine and do the testing. But, 开发者_StackOverflow社区if I don't have different kinds of Engines, it seems inappropriate to parameterize the constructor just to make it testable.

  2. Using a setter and then pass a mock Engine to the setter. Same flow as the above.

  3. Testing the Engine first and then testing the Car with proven Engine (or using a stub Engine).

What are the alternatives I have to make above code testable? What are the strenghts and weaknesses of each method?


Look at it from a different (Test-Driven Development) viewpoint: code that is easy to test is easy to use. Writing the unit tests is actually you testing the "public interface" of your code. If it's hard to test, that's because you've got some dependencies in there that make it hard. Do you really need a containment relationship, or would an associative relationship make more sense?

In your case I personally think it would be more testable to pass the Engine in the constructor, so I'd refactor the constructor as in your suggestion #1. You can test the Engine in one test suite, and provide a mock Engine to test the Car in another test suite. Testing it is now easy, meaning the interface is easy to use. That's a good thing.

Now think about how you would use that implementation in a real project. You'd create a CarFactory class, and the factory would create an Engine and put it in the Car before delivering it to you. (Also notice how this ends up more closely modeling the real world of cars and engines and factories, but I digress.)

Therefore the TDD answer would be to refactor the code to take an Engine pointer on the constructor.


If you only have one Engine type, why are you trying to make it a new object? If you don't plan on swapping engines, don't create another abstraction layer. Just make the engine part of the car.

You might be decomposing to reduce complexity, rather than to reuse components. Good call. In which case, I'd say that 3 is your best bet - validate your lower level components, then use higher level code that calls the lower level objects.

In reality, Engine is more likely to be something like Database. And you will want to change your constructors to use a different Database (for test reasons, or other reasons), but you can leave that lie for a while.


Unit Testing, as it's name implies, is about testing the Units to confirm that they work as prescribed. This means, you should Test engine separately.

System Testing or Integration Testing is about testing that they all 'glue' together correctly.

Of course, it is more complex than just this, but it should point you in the right direction.


The option 1 is generally the right way.

By being able to have full control of the engine you give to the car, you are able to test the car v. well.

You can more easily test how the car behaves with all the different outputs the engine gives to the car. You can also make sure the car its making the appropriate calls to the engine.

Having it in the constructor makes it really clear that the Car depends on an Engine to work. Use it with a dependency injection framework, and the constructor issue isn't really a problem at all.


The real question is, what are the requirements? If the goal is to simply implement a "Car" object then it doesn't even need an engine.

Tests should always be related to requirements. Any object model is going to be some generalization of reality, so the issue is what aspects are required to be represented.

Once you have your requirements down pat, then you should write your tests, at a generic high level. Then, the OO design should be done in such a way that these tests can be implemented.


Misko Hevery writes frequently on this topic. Here's a presentation from October 2009. He argues that the dependency graph should be explicit in the constructor.

it seems inappropriate to parameterize the constructor just to make it testable

I think that's a fascinating comment. Under what sorts of conditions would including testability as part of the design be inappropriate? Sacrificing correctness for testability is clearly wrong, though in practice I have never seen that choice forced. Sacrificing performance for testability... perhaps that, in some specific cases. Sacrificing code uniformity? I personally would rather change the coding standard, and gradually bring the legacy code into compliance.


One possibility in C++ is to use friendship:

class Car
{
private: //for unit testing
    friend class TestCar; //this class is the unit test suite for the Car class
    Car(Engine* mockEngine); //this constructor is only used by the TestCar class
private:
    Engine* m_engine;
public:
    Car();
    // Rest of the car
};

A second possibility is for the constructor's implementation to use a global/static method for example as follows, and you can change the implementation of this method, either via some config file, or by linking (perhaps dynamic-linking) to different versions of this method:

Car::Car()
{
    m_engine = Engine::create();
}


I'd argue in favor of allowing (via constructor or property) the ability to add an implementation of an Engine to the Car (which is preferably an IEngine).

The Car tests shouldn't actually care what the Engine does, so long as it responds properly to the results from calls to the Engine. Then, use a fake Engine to test so that you can control the signals sent to the car, and you should be good to go.

0

精彩评论

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