1

Python is very specific language which gives developer huge flexibility.

So if I have class like this:

class Car:
    def __init__(self, engine: Engine):
        self._engine = engine  # dependency injection
        self.speed = 0

    def accelerate(self, target_speed: int):
        if self._engine.can_accelerate_to_speed(target_speed):
            self.speed = target_speed

testing it is a piece of cake.

def test_car_acceleration():
    car = Car(engine=MagicMock())
    car.accelerate(100)
    assert car.speed == 100

But what if the engine dependency is hidden?

class SlowCar:
    def __init__(self):
        self.engine = DefaultEngine()  # hidden dependency or maybe implementation detail?
        self.speed = 0

    def accelerate(self, target_speed: int):
        if self.engine.can_accelerate_to_speed(target_speed):
            self.speed = target_speed

What is the right approach for the SlowCar? Let's assume SlowCar is intended to have DefaultEngine mounted by default, but nothing prevents class consumer from doing so:

car = SlowCar()
car.engine = HEMI()

Anyway should I treat SlowCar.engine as dependency or implementation detail? Should I patch it out?

def test_slow_car():
    with patch('path.to.SlowCar.DefaultEngine'):
        car = SlowCar()
        car.accelerate(100)
    assert car.speed == 100

def test_slow_car_way2():
    car = SlowCar()
    car.engine = MagicMock()
    car.accelerate(100)
    assert car.speed == 100

I guess that while SlowCar.engine is "public" field I may consider patching it as it's part of public API so it seems to be clearly a dependency.

But if I would have SlowCar._engine.. A "private" field is not part of public API so it seems more like implementation detail and it should not be mocked/patched. That's internal part of SlowCar which I as a consumer should not care about, right? It's tightly coupled to SlowCar and SlowCar does work with it or does not work at all.

0

1 Answer 1

2

Is DefaultEngine deterministic and side effect free? Will it always be?

Many things can be considered a unit for a unit test but if you can’t predict what the unit will do it’s a poor unit.

That said, a unit can be far more than a single class. We’re testing an abstraction not how it works.

Now if DefaultEngine simply must be mocked... well it’s nicer if it’s the default value of an optional parameter then a hidden dependency anyway.

Now sure, it’s Python. So you can sneak around the back. But now you’re testing the back. It’s better to stick to the front. Because that way the front is the only thing that you have to be consistent about.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.