Spy vs. spy – aka “The two sides of the testing coin”

No Comments

When you ask ten developers about unit testing, you will definitely get at least eleven opinions on how to do testing right. As for every other topic, there is also no silver bullet approach for testing, but there are some practices which have become established over the past years.

As in all other learned customs there are good and bad habits, there are both so-called best practices and also anti-patterns.

Let’s talk a little about bad habits, especially about a really bad habit when using so-called “spies” to prove your business logic. That does not mean that using a spy is bad in general, but every tool can be used in the wrong way.

Because I like the Java programming language I will leverage the Mockito mocking library to demonstrate how a special anti-pattern can turn your beautiful test harness into a block of concrete your application will get stuck in.

That’s how this anti-pattern got its name: Test concrete.

And I also will give you a solution to circumvent this anti-pattern.

Let’s start from the beginning. The three parts of a unit test

Usually a unit test prepares the environment (arrange), executes some business code (act) and afterwards checks whether everything has been done properly (assert).

    @Test
    void exampleAAATest() {
 
        int a = 1;
        int b = 2; // Arrange
 
        int c = a + b; // Act
 
        assertThat(c).isEqualTo(3); // Assert
    }

Sounds easy. But what can we do if our business code handling the “act”-part – maybe it’s a service facade – does not return the object we prepared during the “arrange”-phase? So what if there is no result object to do our assertions during the “assert” step? How can we verify if the facade did its job properly?

Let’s imagine there is some “legacy code” (if we had written it on our own it had a better design, of course 😉 ) providing an object with a state and a facade which is designed to manipulate this object in some way using at least some other service.

    public class SomeObject {
 
        private int counter;
 
        public void increaseCounter() {
            this.counter++;
        }
 
    }
 
    ...
 
    public class SomeService {
 
        void increaseObjectCounter(SomeObject object) {
            object.increaseCounter();
        }
 
    }
 
    ...
 
    public class SomeFacade {
 
        private SomeService service;
 
        public SomeFacade(SomeService service) {
            this.service = service;
        }
 
        void processObject(SomeObject object) {
            service.increaseObjectCounter(object);
        }
 
    }

Now, we want to test if our facade increases the internal object counter properly. So we should write a unit test.

It seems there is a simple solution. A Mockito Spy is a powerful friend. It can help you with testing services which neither return a processed object nor a testable result.

Little helpers

Using Mockito for wrapping an object under test with a spy proxy is very convenient to check afterwards whether the business code handled everything right, even if we don’t have a result object. After a spy is processed, there are check methods like verify() to prove if or how often a certain method was called or which arguments were passed to it (using ArgumentCaptors).

Great, since we have our facade returning nothing and the opportunity to turn an arbitrary object into a spy, it seems we just have to put these things together. But how? Which object should be a spy? Excellent question, and that’s exactly the point where we can decide to choose the “good way” or to start getting stuck in concrete.

Writing tests for crucial business code is important and having a good set of tests can help your project to succeed. On the other side, writing bad tests can increase the effort for future changes, make easy refactoring impossible and harm the entire project although the test author’s intention was good.

Decision needed!

If we want to test the facade it is only on us to choose an object to wrap as a spy -> either the service or the passed object. Let’s just try out both.

We can wrap the service as spy:

    @Test
    void exampleTestWithServiceAsSpy() {
 
        SomeObject objectUnderTest = new SomeObject();
        SomeService service = spy(new SomeService()); // Arrange
 
        new SomeFacade(service).processObject(objectUnderTest); // Act
 
        verify(service, times(1)).increaseObjectCounter(objectUnderTest); // Assert
    }

Or we can wrap the passed object as spy:

    @Test
    void exampleTestWithObjectAsSpy() {
 
        SomeObject objectUnderTest = spy(new SomeObject());
        SomeService service = new SomeService(); // Arrange
 
        new SomeFacade(service).processObject(objectUnderTest); // Act
 
        verify(objectUnderTest, times(1)).increaseCounter(); // Assert
    }

Both approaches look the same, both are green and both are valid test cases. They just ensure everything works fine. But which one is the “right” solution?

With great power comes great responsibility: to spy or not to spy?

If we implement it in the first way (service as spy) the test has to have the knowledge about what is going on within our facade. Obviously the test knows that the underlying service method increaseObjectCounter() is responsible to do the whole work. If we (or some of our colleagues) have to refactor the facade in the future, it is also necessary to adjust the test according to every change – although there is no change in the effective result!

Maybe some other service is now doing the work, or other methods are called. Since our test nailed down internal behavior only changing the business code without changing the test is no longer possible. There is some kind of high coupling between our test and the affected lines of code.

Keeping this in mind I would always tend towards implementing the second approach (passed object as spy), because that’s the more flexible solution. We do not have to care about which service is calling objectUnderTest.increaseCounter() or which code is doing the work, we are just sure that at least someone did it. We can refactor the facade and the underlying services as much as we want without touching the test again, as long as the final result (counter was incremented by … someone) is the same.

A (automated unit) test’s purpose is to proof and ensure (regression) a certain business behavior. It does not exist to tack lines of code.

In most cases, wrapping arguments as spy is the less invasive way to create low coupled tests.

Besides that – take it as memory hook – it feels more natural to send a human spy into the building instead of turning the building into a spy to observe incoming humans, doesn’t it? 😉

Conclusion

Don’t let your tests turn your application into some block of concrete by documenting lines of code. Just verify results and requirements, don’t verify that certain service methods have been called in a predefined order! Stay flexible! 🙂

Kevin Peters

“It’s all about data!” – Every application reads and writes information, so Kevin places great emphasis on the smooth and high-performance handling of these data. He has specialized in the use of the Spring Framework and Hibernate and he is also interested in new technologies. Agile software development and the “Clean Code” concept are further foundations of his work.

Comment

Your email address will not be published. Required fields are marked *