4

Let's say there's a class that processes text, and it gets that text from another class as a buffer. If this buffer class has multiple get methods, like readLine(), readChar(), readCharCode(), how should I choose what method to mock?

Obviously, I can just look at the current implementation of the text processing class and choose to mock the method that is currently being used, but in that case the tests would break if the implementation of the text processing class changed.

Should the whole buffer class be mocked so that the implementation of the text processing class doesn't break the unit tests? This doesn't seem practical.

Should I just mock the method being used, and fix the unit tests whenever the implementation changes?

Edit: I'm using BufferedReader because it's a simple class that people already understand. Assume that I could be using another class created by me that might be more complex.

This could happen with any testing framework but, using a concrete example in Java (jUnit + Mockito), with a BufferedReader, here's a text processing class:

public class TextProcessor
{
    public String process(BufferedReader bufferedReader)
    {
        String result = "";
        String line;

        while ((line = bufferedReader.readLine()) != null)
        {
            // Process the text line by line.
            // (...)
        }       

        return result;
    }
}

The unit test using jUnit/Mockito:

@Test
void multipleLineTextTest()
{
    TextProcessor textProcessor = new TextProcessor();
    BufferedReader bufferedReader = mock(BufferedReader.class);

    when(bufferedReader.readLine()).thenReturn("Multiple", "line", "test", null);

    assertEquals("test line Multiple", textProcessor.process()); // Assuming the text processor inverted the order of the words.
}

If process() was changed to use bufferedReader.read() instead (which reads a single character), the tests would fail, but the implementation could still be correct.

Changing the unit tests as the implementation changes seems to be the way to go, but I still wanted to ask this question to see if there's a better solution, or if I'm looking at it the wrong way.

Thank you.

2
  • 1
    This right here is one of the the prime reasons why the trend in software engineering is towards functional interfaces. If your reader class had only one method, or if you imported it via a functional interface, then there couldn't be any doubt about which method you have to support. Sep 2, 2022 at 13:33
  • @KilianFoth: sounds like a good start for an answer noone else has given so far.
    – Doc Brown
    Sep 4, 2022 at 13:12

5 Answers 5

5

The problem you're describing is typically a sign that you're mocking at the wrong layer. The best candidates for mocking are role interfaces that were specifically designed for the unit under test, and therefore only have methods that make sense for your use case rather than a general-purpose class like BufferedReader. The GOOS book talks about this in detail, and goes so far as to say that you shouldn't mock types that you don't own. Even if you think this is a bit extreme, it's a useful guideline to get you thinking about alternatives:

  • If you're dealing with something simple like the BufferedReader in your example, you can just create a real instance in your test and point it to a string or a test file.
  • If the interaction is more complex, that's when you create a role interface (say MyCustomReader) that encapsulates the behavior that your unit requires. You then mock out this interface when testing TextProcessor, and once you're done implementing that class, you go and implement MyCustomReader separately (which in turn might use BufferedReader or another mechanism to read the content).

Obviously, I can just look at the current implementation of the text processing class and choose to mock the method that is currently being used, but in that case the tests would break if the implementation of the text processing class changed.

This suggests that you're writing the tests after you've implemented the class. If you write your tests first, you get to decide what the interaction looks like before you actually implement it. Ideally, you want to write your tests in a way that allows you to refactor the implementation without breaking the tests. And if the requirements change, you'll start by changing your tests first before updating the implementation.

2
  • 2
    Thanks for referencing the role interfaces, I hadn't read about those yet, and for that last paragraph, as I didn't think about it from a TDD perspective. Regarding the mocking of BufferedReader, I agree that there isn't a need to mock it, I just used that as a simple example with a class that people would be familiar with.
    – krazune
    Sep 4, 2022 at 12:08
  • 1
    I understand BufferedReader was just an example, I did try to keep my answer generic while still referencing your example, so I hope it still applies to whatever use case you're dealing with.
    – casablanca
    Sep 4, 2022 at 20:35
3

When mocking a class in an unit test, how should I handle dependency classes that have multiple similar get methods?

Yeah - when there's more than one way to collaborate with a dependency to achieve some outcome, creating tests that constrain the subject to use a specific set of messages adds friction when you want to refactor the behavior.

The real answer: you choose carefully between writing tests like this, accepting the risk of future costs, or you arrange your design in such a way that you can use other techniques to get good enough results.

For example, consider

public class TextProcessor
{
    public String process(BufferedReader bufferedReader, SomeConsumer consumer)
    {
        String line;

        while ((line = bufferedReader.readLine()) != null)
        {
            consumer.doSomethingCoolWithThisLine(line);
        }       

        return consumer.result();
    }
}

Riddle: if you have a comprehensive test suite for consumer, do you also need a bunch of "unit" tests that isolate TextProcessor::process? Or can we get good enough results via human code inspection plus exercising this code in not isolated tests?

My experience is that the good enough results are good enough, provided that we accept a few constraints:

  • all "complicated" code must be in modules that are easy to test
  • any modules that are difficult to test must be "so simple there are obviously no deficiencies".

That said, it's not necessary to isolate the test subject from stable well behaved components.

TextProcessor::process(BufferedReader) should work correctly with any object that correctly implements the BufferedReader contract. And you should by now be pretty confident that java.io.BufferedReader does implement that contract correctly.

You still want isolation, so your test will normally provide a BufferedReader that is isolated from the world outside the test, but there's nothing wrong with using something "real" there - a BufferedReader that wraps a StringReader, or a BufferedReader that wraps a InputStreamReader that wraps a ByteArrayInputStream that wraps....

process(BufferedReader bufferedReader, SomeConsumer consumer)

Notice that it could well be correct to test this function using a real BufferedReader, but a mocked SomeConsumer, because BufferedReader is part of the standard library and is by now super stable, but SomeConsumer is our new bespoke thing that is currently in development.

Horses for courses.

1

Changing the unit tests as the implementation changes seems to be the way to go, but I still wanted to ask this question to see if there's a better solution, or if I'm looking at it the wrong way.

From my perspective, it's the wrong way. Why do you even have a mock?

Your test right now tests, whether your class will work with a mock. Is that what you want to know? Is that what you want to ensure? No. Your class needs to work with a BufferedReader. And for some reason, you are going to great lengths to make sure it does not get tested that way.

So drop the mock and create a BufferedReader in your test. A real one. Problem solved. As an added benefit, your unit test will actually test whether your program works, not whether you configured your mocking framework correctly.

11
  • Do you use mocks? In what scenarios do you use them? Here I used a simple BufferedReader, with very simple logic, but it could be a more complex object.
    – krazune
    Sep 2, 2022 at 17:20
  • @krazune I'm not a huge expert, but I think a mock should be replacing something external or heavy. when code would normally reach out to a DB, or the network, or a file, the mock keeps everything in memory while otherwise behaving as much like normal as possible. Sep 4, 2022 at 1:42
  • @krazune It also allows you to generate 'fake' data to work against. For the example of a BufferedReader, you might still use one, but base it on a MemoryStream instead of a FileStream for example. Then you can feed it any string you want without needing to create a text file. (Example only... BufferedReader is I guess Java which I don't know offhand; not sure how you would set it up so that the reader read from a string.) Sep 4, 2022 at 1:42
  • +1 as I also think mocking BufferedReader is not a good idea. @krazune I wrote a separate answer detailing when/how I would use mocks.
    – casablanca
    Sep 4, 2022 at 4:29
  • 1
    @nvoigt The value of the unit tests here is that if a test fails, the issue is more localized. If a test that doesn't use mocks fails, it's not as obvious which one of the components failed. I think both types of tests are useful.
    – krazune
    Sep 4, 2022 at 15:23
0

Lets say you update the mock setup each time the test starts to fail due to a change in the underlying method call used.

Eventually you will end up with a mock which mocks all the methods in the interface. An in memory version of your BufferReader class.

If you are going to have lots of tests which use the injected dependency you might want to simply write this in memory version first and use it in the tests, rather than setting up specific mock for each test.

Obviously there is a downside to this, your tests now have a shared dependency on this mock class, which may become over complicated. You may find you still need to specifically mock some tests where you want the dependency to throw an exception or return some edge case.

It's a balancing act, but if you are getting into this problem you might want to consider whether your interfaces are sufficiently segregated.

IBufferReader
{
    ReadAll()
    ReadOne()
    ReadX(int x)
}

Makes sense, all the methods work off the same underlying data, you could write a simple mock class with all three methods and not run into any issues.

INextServiceLayerDown
{
    ReadAll()
    ReadOne()
    ReadX(int x)
    IsItThursday()
    EmailChistamasCards()
}

Starts to look a bit crazy. If you have a single mock for all this its going to need complex setup and couple a whole load of different tests together. If you had split the interface up you would be in a better situation.

0

Obviously, I can just look at the current implementation of the text processing class and choose to mock the method that is currently being used, but in that case the tests would break if the implementation of the text processing class changed.

An interface describes what an implementor does. If the implementor changes its implementation, this can break the contract (i.e. what was defined by the interface that would be done) even though the code still compiles.

For example, I could create a bool StartsWith(string) interface method and an implementation which returns true if today is December 25. Syntactically valid, will compile, but fails to adhere to the spirit of the interface.

In your example which I quoted, the kind of change you are talking about is one where the spirit of the interface is no longer adhered to, which constitutes a breaking change to the interface even though the code might still compile.

Forcing your implementors to adhere to the spirit of a contract is simply not something that you can easily enforce on the current level that you're focused on.
This is something that requires either trust in your developers to always write code that adheres to the spirit of the contract, or significant testing of each and every implementation of an interface to make sure that the spirit is adhered to.

This would not be put on the consumer's unit tests (which is what you're talking about), it requires unit tests for the interface (specifically, each implemenatation of the interface).

Having such tests renders your current question moot. Given that the interface's spirit did not change, but the implementations has, does not lead to your mock implementation being broken, as it (when unchanged) will still be adhering to the spirit of the interface.

If, however, you are talking about willfully changing the spirit of the interface, then a breaking change in all of the consumers (including the mocks in those consumers' tests) are inevitably liable to break.

However, I would consider this bad practice to begin with. If you have a different spirit, you should create a different interface, and not try to repurpose an existing one. When followed, this good practice advice would also render the question moot.

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.