Lets say you have a bunch of classes that call each other like this:
1 |
Root -> Middle -> End -> Database |
Here, Root calls Middle which calls End which calls Database . If you would like to test from Root , and want to mock out the Database how can you do that in Spring? Spring doesn’t make this easy. But I have figured out how to do this in a way that I consider satisfactory.
Lets look at at my project layout:
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>mavensnapshot</groupId> <artifactId>mavensnapshot</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>mavensnapshot</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.9.5</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.1.4.RELEASE</version> <!--It must be Spring 4.x :( --> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.1.4.RELEASE</version> <!--It must be Spring 4.x :( --> </dependency> </dependencies> </project> |
beans.xml
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:annotation-config /> <context:component-scan base-package="com.sleepeasysoftware"> <context:exclude-filter type="assignable" expression="com.sleepeasysoftware.springdeepmock.NoAutowire"/> </context:component-scan> </beans> |
Root.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.sleepeasysoftware.springdeepmock; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Root { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("/beans.xml"); //no particular reason for using xml based configuration here //I just wanted to show that you can useMiddle(context.getBean(Middle.class)); } private static void useMiddle(Middle middle) { middle.useEnd(); } } |
Middle.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.sleepeasysoftware.springdeepmock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class Middle { @Autowired private End end; public void useEnd() { end.useDatabase(); } } |
End.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.sleepeasysoftware.springdeepmock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class End { @Autowired private Database database; //in my test, I want this to be a mock public void useDatabase() { database.save(); } } |
NoAutowire.java
1 2 3 4 |
package com.sleepeasysoftware.springdeepmock; public interface NoAutowire { } |
Database.java
1 2 3 4 5 6 7 8 9 10 11 12 |
package mavensnapshot; import org.springframework.stereotype.Service; @Service public class Database { public void save() { System.out.println("production code"); //pretend this saves to a database. I do not want this to occur in my test! } } |
MockDatabaseTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
package com.sleepeasysoftware.springdeepmock; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.*; import org.springframework.context.support.ClassPathXmlApplicationContext; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; public class MockDatabaseTest { @Test public void testReplaceRepositoryWithMock() { ApplicationContext context = new AnnotationConfigApplicationContext(Config.class); Database mockRepo = context.getBean(Database.class); Middle middle = context.getBean(Middle.class); middle.useEnd(); verify(mockRepo).save(); } @Configuration @ImportResource("classpath:/beans.xml") //import the beans.xml configuration, I definitely don't want: //1) A separate config file for each test //2) To define all these beans by hand public static class Config implements NoAutowire { @Bean @Primary //given two beans of the same type, let this one win //that means this config will give you back a mock database //not the real one // //because this config is only used for tests, this @Primary won't interfere with //production code public Database getDatabase() { return mock(Database.class); } } @Test /** This test proves that we can still get the real Database class if we want to. This test shows another tricky detail. beans.xml is component-scan for the com.sleepeasysoftware package and this test is in that package. Unfortunately, this means when this test is on the classpath (because you decide to run it) then the component-scan will find the above @Configuration and use it. This prevents us from getting the real Database in another test even if we want to for a good reason (e.g., an end to end test). Here's how we solve this problem: We create a blank interface named NoAutowire and we make Config implement it. Then, we configure the beans.xml component-scan and tell it to ignore any class that implements this interface. */ public void testCanGetRealDatabaseInOtherTests() throws Exception { ApplicationContext context = new ClassPathXmlApplicationContext("/beans.xml"); Database realRepo = context.getBean(Database.class); assertEquals(Database.class, realRepo.getClass()); } } |
What I like about this solution most is that it keeps the relevant part of the configuration in the test class itself and the rest of the configuration (the @ImportResource ) out of sight. Here’s what I like about this compared to the alternatives I know of:
- Springockito has bugs.
- Creating a new xml file each time I want a different mock is annoying because I’ll be creating so many.
- Using a master test xml file will be painstaking to maintain, especially if I decide I want to use the real database in a specific test.
- I don’t think you can initialize a mock in a Spring xml file anyway, at least I haven’t found a way to do it.
- Mockito’s @InjectMocks will not Spring-ify the class it works on.
- The @Profile annotation mixes too much test and production knowledge together into my configs. My solution keeps the production configuration ignorant of the test configuration.
- If you simply traverse from Root to End to manually replace the Database with a mock, then your tests will become fragile and break when you try to move this call ordering around. In other words, the test becomes too intimately aware of the way the classes interact with each other and will give you false positive errors.
Some might say that I should swap Database out with an in memory database. Some times that make sense, but other times it doesn’t. Database doesn’t have to be a “Database”. It can be an API. It’s not easy to replace an arbitrary API with an in memory API. That’s where this technique comes in handy.
Others will say, “you shouldn’t write your tests this way”. Whether that’s wrong or right does not matter. This article is about showing you how you can do it if you want to, it’s not necessarily a best practice.
If you would like to look at this source code, it is on github here: https://github.com/SleepEasySoftware/SpringDeepMock