2

I was working on a unit test that has a mock object looking something like:

public class TestMock
{
    static {
        Doodad a = new Doodad(0);
        Doodad b = new Doodad(1);
        Doodad c = new Doodad(2);
  
        Foobar x = new Foobar('x');
        Foobar y = new Foobar('y');
        Foobar z = new Foobar('z');

        List<Doodad> doodads = Arrays.asList(a, b, c);
        Map<String, Foobar> foobars = new HashMap<>();
        foobars.put(x.getName(), x);
        foobars.put(y.getName(), y);
        foobars.put(z.getName(), z);
    }

    public static final SomeObj TestObj = new SomeObj('Test', doodads, foobars);
}

In practice, Map<String, Foobar> is parsed from a CSV and may be thousands of these objects, so instead of mocking 1000 new Foobar(...) in this mock object, I only mocked the first entry, the middle entry (so entry 500) and the last entry. In test this would be asserted as:

public class CsvOpTest
{
    @Test
    public void testCsvParse() {
        final SomeObj parsedObj = CsvTools.parse(CSV_FILE_CONST);
        final SomeObj testObj = TestMock.TestObj;

        // other test details about Doodad
        
        final List<String> keys = testObj.getFoobars().keySet().toArray();

        final String first = keys[0];
        final String middle = keys[1];
        final String last = keys[2];

        assertEquals(parsedObj.getFoobars().get(first), testObj.getFoobars().get(first));
        assertEquals(parsedObj.getFoobars().get(middle), testObj.getFoobars().get(middle));
        assertEquals(parsedObj.getFoobars().get(last), testObj.getFoobars().get(last));
    }
}

A colleague around the same level as me asked why I didn't just mock the 1000 objects so that its an true accurate representation of the entire object we expect to parse, thus making the test:

public class CsvOpTest
{
    @Test
    public void testCsvParse() {
        final SomeObj parsedObj = CsvTools.parse(CSV_FILE_CONST);
        final SomeObj testObj = TestMock.TestObj;

        assertEquals(parsedObj, testObj);
    }
}

And I see the validity in this. So I got to wondering, is there any issue with the assumptions I made in my mock? In my eyes, if the "range" of parsed entities are correct, then all the entities in between must also be correctly parsed otherwise the parser would've failed. Similarly to how we don't test private methods exclusively, but rather via public callers that call private methods internally.

Is there any merit or issue with my approach or should one always mock an object to 100% accuracy in its entirety without assumptions?

10
  • You're not testing the mock; the mock is just a part of the arrange step for each test. That is, It's just a part of the test case setup, it's there to support the test case itself. The mock isn't supposed to represent accurately the real thing, it's just supposed to have the same interface - to look the same from the perspective of the system/class/function under test. That's why it's a mock. The mock itself shouldn't really do anything, it doesn't need to have any logic - it just need to return a canned result that you've configured it with (for that particular test case), ... 1/2 Commented Aug 1, 2022 at 6:20
  • ... and maybe let you spy on some methods of interest. E.g., if the real dependency you're mocking is using some input to do a calculation, make a decision, and then return a result, the mock can skip all that logic and not even look at the input, if that's not the point of the test - it can just return what you told it to. It doesn't need to actually work the way the real thing works. Again, you're not testing the mock, you're just setting up these highly controlled scenarios of various interactions with the dependency. 2/2 Commented Aug 1, 2022 at 6:20
  • P.S. That said, the kind of test you wrote (as well as the one that your colleague suggested) is more of a quick and dirty safety-net test that compares a bunch of predefined inputs to expected outputs. Which is fine, but it doesn't really test behavioral characteristics of parse, and your mock is not really a mock (of a dependency), it's just a collection of expected results - that's not what we refer to as a mock. It doesn't tell you if parse will behave correctly on unexpected inputs, etc. And you have to keep it in sync with your CSV file. Commented Aug 1, 2022 at 6:33
  • @FilipMilovanović I've updated the title of this post, I understand it was unclear as I am not testing the mock but rather using it in unit tests. As to your final comment, yes I am using this object to ensure the final object produced by CsvTools.parse(CSV_FILE_CONST); matches as expected; however the resultant there would have >1000 objects stored in Map<String, Foobar> in practice, but I wanted to reduce the testing profile. 1/2
    – pstatix
    Commented Aug 1, 2022 at 19:45
  • @FilipMilovanović I also only showed a single test, to you points about the characteristics of parse that handle malformed files, incorrect parsing, etc. Those tests are defined as well, I just wanted to show in one of the tests that the desired objects match a predefined one. Can you expand on what you meant by "that's not what we refer to as a mock"? 2/2
    – pstatix
    Commented Aug 1, 2022 at 19:47

2 Answers 2

0

Is part of the requirements that your CSV will contain exactly 1000 lines every time?

If it isn't, then the total amount of lines is not a given, and you can inherently not account for it in a test.
Your mocked data can be whatever you want it to be; you don't even need to model it on your real data (which is what you implied when you said "I only mocked the first entry, the middle entry and the last entry."). In many cases, you could even get away with only mocking a single line, unless you are performing some tests that inherently only work when there are multiple lines.

However, if it is a concrete requirement that your file will always contain exactly the same amount of lines, then there can be value in ensuring that the code handles exactly that amount of lines, and thus to create a mock that simulates this.

Note, however, that there's a difference between a requirement and something that happens to be true but was not written down as a requirement. If your requirements do not specify a constant line count, then it's not a requirement, even if the real import files happen to always be the same size right now.

3
  • 1
    The files are indeed of fixed lengths, so we don't need to worry about variant length files (in the case of mocking this particular object; other objects may only have 500 lines but will always have 500 lines). The question is really around whether or not a mock object, one representing expected results, needed to be a complex representation or rather a partial one to ensure accuracy of the operation. 1/2
    – pstatix
    Commented Aug 2, 2022 at 23:13
  • If mocking needs to be an entire representation, then the mock becomes huge, and for that may be too verbose. 2/2
    – pstatix
    Commented Aug 2, 2022 at 23:17
  • @pstatix No one said you can't have your mocked data generated using a loop of some kind. If the only way to test your data is to have 1000 intricately connected and individually/manually managed values; then your code is not test-friendly enough. If you can test parts of the logic independently, then you should do so in order to simplify each individual test.
    – Flater
    Commented Aug 3, 2022 at 6:58
0

Unit tests and mocked data should represent potential failure cases. Simple example, a well know bug in the implementation of binary search is to have an integer overflow with the calculation m = (l + r) / 2, if l and r are large enough. If implementing a binary search and you want to verify that your code does not contain this bug, then assuming at least 8 bit ints, doing a binary search on 30 items is useless. Your mocked array of fake data needs to contain enough items so that l + r > int.Max.

It’s not really uncommon to have multiple entities/objects in a unit test, but it’s more common to just have one.

So, to answer your title question: a test should have a subset of the mocked data sufficient to exercise a particular scenario / test case. Whether that is 1 or billions, depends upon what you are testing.

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.