0

In The Art of Unit Testing, 2nd Ed., the author gives the following example for injecting a stub using constructor injection and a "fake object". The goal of the "fake object" is to inherit the interface and break dependencies so that way you can unit test it.

I don't understand how these are examples of legitimate tests. The call to LogAnalyzer.IsValidLogFileName() used at runtime would call some implementation of IExtentionManager.IsValid(), which would interrogate the string in some way before return a bool or throwing an exception. So to me, the significant test here is: for IsValidLogFileName(), does the method return what you expect when you give it a specific string?

In the examples below, the author hard-codes and sets myFakeManager.WillBeValid within the tests to true, passes myFakeManager to the LogAnalyzer and immediately asserts that what they just set myFakeManager.WillBeValid to is true... What they literally just set it to?!

How are these tests in LogAnalyzerTests.cs useful?

LogAnalyzer.cs

public class LogAnalyzer
{
    private IExtensionManager manager;
    public LogAnalyzer(IExtensionManager mgr)
    {
        manager = mgr;
    }

    public bool IsValidLogFileName(string fileName)
    {
        return manager.IsValid(fileName);
    }
}

IExtensionManager.cs

public interface IExtensionManager
{
    bool IsValid(string fileName);
}

FakeExtensionManager.cs

internal class FakeExtensionManager : IExtensionManager
{
    public bool WillBeValid = false;
    public Exception WillThrow = null;

    public bool IsValid(string fileName)
    {
        if (WillThrow != null)
            throw WillThrow;

        return WillBeValid;
    }
}

LogAnalyzerTests.cs

[Test]
public void IsValidFileName_NameSupportedExtension_ReturnsTrue()
{
    FakeExtensionManager myFakeManager = new FakeExtensionManager();
    myFakeManager.WillBeValid = true;

    LogAnalyzer log = new LogAnalyzer(myFakeManager);

    bool result = log.IsValidLogFileName("short.ext");
    Assert.True(result);
}

[Test]
public void IsValidFileName_ExtManagerThrowsException_ReturnsFalse()
{
    FakeExtensionManager myFakeManager = new FakeExtensionManager();
    myFakeManager.WillThrow = new Exception("this is fake");

    LogAnalyzer log = new LogAnalyzerConstructor(myFakeManager);
    bool result = log.IsValidLogFileName("anything.anyextension");
    Assert.False(result);
}
2
  • That entire section is about isolation techniques - think of that code example as of an intermediary step during the restructuring of your test. Fakes replace external dependencies. The book goes on to make a distinction between fakes being used as stubs and mocks. Stubs are never tested themselves (and they cannot cause the test to fail); their purpose, other then isolation, is to provide contextual setup so that you can test the other class under the specific conditions given in part by the stub (and in part by other setup code in the test itself). Apr 7, 2020 at 9:31
  • *By "other class" I mean the class under test - the one that uses a fake as a dependency Apr 7, 2020 at 9:31

3 Answers 3

2

Unfortunately, in order to demonstrate an idea in software development, authors tend to boil down the examples to their simplest forms so that the idea is not clouded by incidental complexity.

In this case however this is what's called "breaking the seams" (check out Michael Feathers' Working Effectively with Legacy Code) where you pull out logic that interfaces with low-level constructs (like the file system) into another logical unit. Verify that the original code uses the new interface, and put in tests for the new service.

Your tests become simple for the original unit. And the verification that the new unit works properly belongs in the tests for the new unit. Using your example specifically.

  1. We break the seam of the LogAnalyzer by introducing the IExtensionManager.
  2. We validate in the unit tests for LogAnalyzer that it is delegating it's needs to the IExtensionManager vis a vis a fake (including that the Analyzer properly handles exceptions)
  3. (Not shown in your example) We make unit tests (or integration tests) for the concrete IExtensionManager (e.g. a FileExtensionManager).

Basically if anything breaks we know to look at the FileExtensionManager for errors.

2

Using an example

The goal of the "fake object" is to inherit the interface and break dependencies so that way you can unit test it. [..] I don't understand how these are examples of legitimate tests.

Think of it this way: your car won't start. A car has many parts, and if one of them fails, the car won't start. But which part is failing at the moment?

One way of finding out which part is failing, is by taking the car apart and testing each part individually. This is what a unit test is. So let's continue the example and let's test if the piston is faulty.

A piston is a device which takes in air and fuel, and outputs kinetic energy (= movement). The fuel is retrieved from the fuel tank, the air is retrieved from the air intake. So an analogous code example would be:

public class Piston
{
    private readonly AirSource airSource;
    private readonly FuelSource fuelSource;

    public Piston(AirSource airSource, FuelSource fuelSource)
    {
        this.airSource = airSource;
        this.FuelSource = fuelSource;
    }

    public Movement Fire()
    {
        var fuel = fuelSource.GetFuel();
        var air = airSource.GetAir();

        // This is just some silly example code
        if (fuel != null && air != null)
            return new Movement();
        else
            throw new EngineStallException();
    }
}

Don't get too bogged down with the actual content of the method - it's silly code that I kept simple because the details don't matter. The main goal is to understand that the fuel and air sources are dependencies, and the piston's behavior is influenced by the behavior of its connected fuel and air source.

In the real car (= production code), the piston is connected to the car's fuel tank and air intake, along the lines of:

var piston = new Piston(myCar.AirIntake, myCar.FuelTank);

However, and this is key to understanding unit testing: when we are unit testing the piston, we don't want to use the real dependencies, because any issues in the dependencies (e.g. a faulty air intake) will lead us to wrongly conclude that there's something wrong with the piston (because it depends on the air intake), even though there's nothing wrong with the piston itself and it would be working if its air intake was working properly.

Unit testing is done by swapping out the dependencies for ones where you can guarantee that they are working correctly. If you know for a fact that the dependencies are not faulty, then any faulty behavior from the piston proves that there is a problem in the piston itself.

So when we test the piston, we do something like this:

var piston = new Piston(new InfiniteAirSource(), new InfiniteFuelSource());

var movement = piston.Fire();

Assert.IsNotNull(movement);

This is our simple unit test: when the piston is provided with air and fuel, is it able to generate movement?

  • If the test passes, then the piston is working as intended.
  • If the test fails, then there is an issue with the piston.

The test result immediately answers the question "does the piston work?". Compare this to when we hadn't mocked our dependencies:

var piston = new Piston(myCar.AirIntake, myCar.FuelTank);

var movement = piston.Fire();

Assert.IsNotNull(movement);
  • If the test passes, then the piston and its dependencies are working as intended.
  • If the test fails, ...
    • maybe the piston failed?
    • maybe the air intake failed?
    • maybe the fuel tank failed?

This test no longer answers the question "does the piston work?", because when the test fails, we don't actually know if the piston is to blame or not.

Remember, our car won't start, and we still haven't figured out why it won't start. But we know that it won't start, which means that something isn't working. If we perform unit tests on all of the car's parts, then (at least) one of these unit tests will fail. This will reveal to us which part we need to fix in order to get the car working again.

That is the purpose of unit tests: figuring out which specific component is faulty.

Small note: unit vs integration tests

Note that there is still some value to be had from testing multiple parts together, but that's called an integration test, not a unit test.

My opinion is that integration tests are only meaningful when all involved components are individually unit tested before the combined integration test is run, but that's a discussion for a different day.


Your test case

In the examples below, the author hard-codes and sets myFakeManager.WillBeValid within the tests to true, passes myFakeManager to the LogAnalyzer and immediately asserts that what they just set myFakeManager.WillBeValid to is true... What they literally just set it to?!

You're not thinking of this the right way. You know what the content of the IsValidLogFileName method is, and you're thinking that everybody knows that. They don't.

You are able to tell me if your car starts or not, because you understand its public behavior (turn the key => start the car) and can tell that something is wrong (I turn the key but the car won't start).
However, you are not a mechanic and don't know how a car is built, or what could be causing it to not start. But you don't need to know how the car works to test if the car works.

Unit tests only care about external behavior, they don't care about implementation.

and immediately asserts that what they just set myFakeManager.WillBeValid to is true...

The test does not assert that myFakeManager.WillBeValid returns true.

The test asserts that a LogAnalyzer behaves a certain way ("the car starts") when its dependencies are in a particular state ("the key is turned").

In this particular case, the behavior is purely defined by the value of IExtensionManager.WillBeValid, but that is not always going to be the case for everything you test.


The value of testing

How are these tests in LogAnalyzerTests.cs useful?

Even if the method merely passes a value from its dependency to its consumer, the test still has value, as the test effectively confirms that a LogAnalyzer correctly listens to its dependencies and behaves as expected.

A student attends a class, learns something, and then takes an exam on the subject matter they learned about.

Based on the question you posted here, your interpretation would be that this exam is useless since it tries to assert that the student knows the subject matter that they've learned - and it's pointless because (allegedly) having attended a class on the subject inherently means that you know the subject matter. That's clearly not the right idea here.

You're missing the point of the test. The test is there to confirm that the information passed from one source (the teacher / the extension manager) to another (the student / the consumer of the LogAnalyzer object).

When the test passes, that proves to you that the student correctly learned the lesson that the teacher provided. In your test case, it proves to you that the LogAnalyzer actually uses its dependencies the way it is expected to use them.

1

A few things wrong with these tests:

  • The fake class throws an exception, nothing catches it, and the SUT (IsValidLogFileName) is expected to return false in that case.
  • The filename arguments to IsValidLogFileName aren't actually used. You could change them to any strings and these tests would still pass.
  • The tests just verify that the SUT delegates to a dependency, which doesn't seem to add a lot of value.

The value of dependency injection for testing might be easier to see in these examples if the IsValidLogFileName method contained more logic and the fake dependency was just a part of that logic. Because the SUT is so trivial, these tests are really just asserting that the IsValidLogFileName method defers to the value given by the IExtensionManager for determining if a filename is valid.

To your main question, I don't think that's super useful. But if I did want to test that delegation to the dependency, I would use a mock object instead of a fake. With a mock, you can set expectations on which methods are called and with which arguments, so you can assert that LogAnalyzer actually calls the IExtensionManager.isValid method with the same filename given in the test. That way, you verify that the filename arguments sent in from the tests are actually used.

Looking a little further into the book that these examples are from, it seems you're not the only one to encounter issues like this:

The code examples, which could have significantly redeemed the book, are a disaster. They are full of mistakes that the reader must mentally correct.

I highly recommend XUnit Test Patterns as an alternative resource. Their website is great too, and I believe has most (all?) of the same information. I found the illustrations of the individual test-double patterns particularly useful.

Your Answer

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

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