Injecting Test Doubles in Spring using Mockito and BeanPostProcessors
I'm pretty sure that if you have ever used Spring and are familliar with unit testing, you have encountered a problem related to injecting mocks / spies (Test Doubles) in the Spring's application context which you wouldn't want to modify. This article presents an approach how to solve this issue using Spring's components.
Project structure
Let's start with the project structure:As usual to present a problem I'm trying to show a very simple project structure. The approach that I'm about to show could show more benefits if I made the problem more extensive as we had in our project:
- we had dozens of interfaces and implementations autowired to lists
- we wanted to perform some functional tests basing on the existing Spring application context
- we wanted to verify that for certain input conditions some specific implementation would have their methods executed
- we wanted to stub database access.
In this example we have a PlayerService that gets a Player using a PlayerWebService. We have an applicationContext that simply defines packages for autowiring:
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>Then we have our very simple model:
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:context="https://www.springframework.org/schema/context"
xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.blogspot.toomuchcoding"/>
</beans>
Player.java
package com.blogspot.toomuchcoding.model;
import java.math.BigDecimal;
/**
* User: mgrzejszczak
* Date: 08.08.13
* Time: 14:38
*/
public final class Player {
private final String playerName;
private final BigDecimal playerValue;
public Player(final String playerName, final BigDecimal playerValue) {
this.playerName = playerName;
this.playerValue = playerValue;
}
public String getPlayerName() {
return playerName;
}
public BigDecimal getPlayerValue() {
return playerValue;
}
}
the implementation of the PlayerService that uses PlayerWebService to retrieve data regarding the Player:
PlayerServiceImpl.java
package com.blogspot.toomuchcoding.service;
import com.blogspot.toomuchcoding.model.Player;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* User: mgrzejszczak
* Date: 08.06.13
* Time: 19:02
*/
@Service
public class PlayerServiceImpl implements PlayerService {
private static final Logger LOGGER = LoggerFactory.getLogger(PlayerServiceImpl.class);
@Autowired
private PlayerWebService playerWebService;
@Override
public Player getPlayerByName(String playerName) {
LOGGER.debug(String.format("Logging the player web service name [%s]", playerWebService.getWebServiceName()));
return playerWebService.getPlayerByName(playerName);
}
public PlayerWebService getPlayerWebService() {
return playerWebService;
}
public void setPlayerWebService(PlayerWebService playerWebService) {
this.playerWebService = playerWebService;
}
}
the implementation of the PlayerWebService that is a provider of data (in this scenario we are simulating awaiting for response):
PlayerWebServiceImpl.java
package com.blogspot.toomuchcoding.service;Perhaps the project structure and the methods are not one of the most brilliant you have ever seen but I wanted to keep it simple to present the problem ;)
import com.blogspot.toomuchcoding.model.Player;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* User: mgrzejszczak
* Date: 08.08.13
* Time: 14:48
*/
@Service
public class PlayerWebServiceImpl implements PlayerWebService {
private static final Logger LOGGER = LoggerFactory.getLogger(PlayerWebServiceImpl.class);
public static final String WEB_SERVICE_NAME = "SuperPlayerWebService";
public static final String SAMPLE_PLAYER_VALUE = "1000";
@Override
public String getWebServiceName() {
return WEB_SERVICE_NAME;
}
@Override
public Player getPlayerByName(String name) {
try {
LOGGER.debug("Simulating awaiting time for a response from a web service");
Thread.sleep(5000);
} catch (InterruptedException e) {
LOGGER.error(String.format("[%s] occurred while trying to make the thread sleep", e));
}
return new Player(name, new BigDecimal(SAMPLE_PLAYER_VALUE));
}
}
The problem
So what actually is the problem? Let us assume that we want our autowired PlayerWebServiceImpl to be a Spy that we can verify. What is more you don't want to actually change anything in the applicationContext.xml - you want to use the current version of the Spring context.With mocks it's easier since you can define in your XML file (using Mockito factory method) your bean as a mock to override the original implementation just like this:
<bean id="playerWebServiceImpl" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="com.blogspot.toomuchcoding.service.PlayerWebServiceImpl"/>
</bean>
What about the Spy? It's more problematic since in order to create a Spy you need an already existing object of the given type. In our example we have some autowiring going on so we would have to first create a spring bean of the PlayerWebService type (Spring would have to wire all its dependencies) and then wrap it around with Mockito.spy(...) and only then would it have to be wired somewhere else... It's getting very complicatied doesn't it?
The solution
You can see that the problem is not that trivial to be solved. An easy way to fix it however is to use native Spring mechanisms - BeanPostProcessors. You can check my article about how to create a Spring BeanPostProcessor for a specified type - we'll be using it in this example.Let's start with checking the test class:
PlayerServiceImplTest.java
package com.blogspot.toomuchcoding.service;In this test we want to mock retrieval of Player from the PlayerWebService (let's assume that normally it would try to send a request to the outside world - and we wouldn't want that to happen in our scenario) and test that our PlayerService returns the Player that we provided in the method stub and what is more we want to perform verification on the Spy that the method getWebServiceName() has been executed and that it has a very precisely defined return value. In other words we wanted to stub the method getPlayerByName(...) and wanted to perform verification of the spy by checking the getWebServiceName()method.
import com.blogspot.toomuchcoding.model.Player;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.math.BigDecimal;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.doReturn;
import static org.mockito.Mockito.verify;
/**
* User: mgrzejszczak
* Date: 08.06.13
* Time: 19:26
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:testApplicationContext.xml")
public class PlayerServiceImplTest {
public static final String PLAYER_NAME = "Lewandowski";
public static final BigDecimal PLAYER_VALUE = new BigDecimal("35000000");
@Autowired
PlayerWebService playerWebServiceSpy;
@Autowired
PlayerService objectUnderTest;
@Test
public void shouldReturnAPlayerFromPlayerWebService(){
//given
Player referencePlayer = new Player(PLAYER_NAME, PLAYER_VALUE);
doReturn(referencePlayer).when(playerWebServiceSpy).getPlayerByName(PLAYER_NAME);
//when
Player player = objectUnderTest.getPlayerByName(PLAYER_NAME);
//then
assertThat(player, is(referencePlayer));
verify(playerWebServiceSpy).getWebServiceName();
assertThat(playerWebServiceSpy.getWebServiceName(), is(PlayerWebServiceImpl.WEB_SERVICE_NAME));
}
}
Let's check the test context:
testApplicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="applicationContext.xml"/>
<bean class="com.blogspot.postprocessor.PlayerWebServicePostProcessor" />
</beans>
The test context is very small since it's importing the current applicationContext.xml and creating a Bean that is the key feature in this example - the BeanPostProcessor:
PlayerWebServicePostProcessor.java
package com.blogspot.postprocessor;The class is extending the AbstractBeanPostProcessor that implements the BeanPostProcessor interface. The logic behind this class is to register the Class for which one wants to perform some actions either before initialization (postProcessBeforeInitialization) or after initialization of the bean (postProcessAfterInitialization). The AbstractBeanPostProcessor is well explained in my post Spring BeanPostProcessor for a specified type but there is one slight change - in my old post we were allowed by the abstraction to perform some actions on the bean without the possibility of returning a wrapper or a proxy on the bean.
import com.blogspot.toomuchcoding.processor.AbstractBeanPostProcessor;
import com.blogspot.toomuchcoding.service.PlayerWebService;
import static org.mockito.Mockito.spy;
/**
* User: mgrzejszczak
* Date: 07.05.13
* Time: 11:30
*/
public class PlayerWebServicePostProcessor extends AbstractBeanPostProcessor<PlayerWebService> {
public PlayerWebServicePostProcessor() {
super(PlayerWebService.class);
}
@Override
public PlayerWebService doBefore(PlayerWebService bean) {
return spy(bean);
}
@Override
public PlayerWebService doAfter(PlayerWebService bean) {
return bean;
}
}
As you can see in the case of PlayerWebServicePostProcessor before initialization we are creating a Spy using Mockito.spy(...) method. In this way we create a factory hook on the intialization of beans of given type - it's as simple as that. This method will be executed for all the classes that implement the PlayerWebService interface.
Other possibilities
While checking out current solutions to this problem I have encountered the Springockito library by Jakub Janczak.
I haven't been using this so I don't know what are (if there are any ;) ) production issues related to this library but it seems really nice and intuitive - great job Jakub! Still, you become dependent on the external library whereas in this example I've shown how to deal with the issue using Spring.
Summary
In this post I've shown how to
- create mocks for existing beans using XML Spring configuration
- create a BeanPostProcessor implementation that performs logic for a given class of beans
- return Spy (you could also return a Mock) for the given class of bean
Now let's move through the Prons and Cons of my approach:
Advantages
- you use Spring native mechanism to create Test Doubles for your beans
- you are not required to add any additional external dependencies
- if you use the AbstractBeanPostProcessor you have very little changes to implement
Disadvantages
- you have to be familliar with internal Spring architecture (that it uses BeanPostProcessors) - but is it a disadvantage? ;) - in fact if you use the AbstractBeanPostProcessor you don't have to be familliar with it - you just have to provide the class type and actions to happen before and after initialization.
- it's less intuitive than annotations like in the Springockito library
Sources
The sources are available at TooMuchCoding BitBucket repository and TooMuchCoding Github repository.Spock - return nested spies / mocks
Hi! Some time ago I have written an article about Mockito and using RETURNS_DEEP_STUBS when working with JAXB. Quite recently we have faced a similliar issue with deeply nesetd JAXB and the awesome testing framework written in Groovy called Spock. Natively Spock does not support creating deep stubs or spies so we needed to create a workaround for it and this article will show you how to do it.
Project structure
We will be working on the same data structure as in the RETURNS_DEEP_STUBS when working with JAXB article so the project structure will be quite simillar:As you can see the main difference is such that the tests are present in the /test/groovy/ folder instead of /test/java/ folder.
Extended Spock Specification
In order to use Spock as a testing framework you have to create Groovy test scripts that extend the Spock Specification class. The details of how to use Spock are available here. In this project I have created an abstract class that extends Specification and adds two additional methods for creating nested Test Doubles (I don't remember if I haven't seen a prototype of this approach somewhere on the internet).ExtendedSpockSpecification.groovy
package com.blogspot.toomuchcoding.spock;These two methods work in a very simillar manner.
import spock.lang.Specification
/**
* Created with IntelliJ IDEA.
* User: MGrzejszczak
* Date: 14.06.13
* Time: 15:26
*/
abstract class ExtendedSpockSpecification extends Specification {
/**
* The method creates nested structure of spies for all the elements present in the property parameter. Those spies are set on the input object.
*
* @param object - object on which you want to create nested spies
* @param property - field accessors delimited by a dot - JavaBean convention
* @return Spy of the last object from the property path
*/
protected def createNestedSpies(object, String property) {
def lastObject = object
property.tokenize('.').inject object, { obj, prop ->
if (obj[prop] == null) {
def foundProp = obj.metaClass.properties.find { it.name == prop }
obj[prop] = Spy(foundProp.type)
}
lastObject = obj[prop]
}
lastObject
}
/**
* The method creates nested structure of mocks for all the elements present in the property parameter. Those mocks are set on the input object.
*
* @param object - object on which you want to create nested mocks
* @param property - field accessors delimited by a dot - JavaBean convention
* @return Mock of the last object from the property path
*/
protected def createNestedMocks(object, String property) {
def lastObject = object
property.tokenize('.').inject object, { obj, prop ->
def foundProp = obj.metaClass.properties.find { it.name == prop }
def mockedProp = Mock(foundProp.type)
lastObject."${prop}" >> mockedProp
lastObject = mockedProp
}
lastObject
}
}
- Assuming that the method's argument property looks as follows: "a.b.c.d" then the methods tokenize the string by "." and iterate over the array - ["a","b","c","d"].
- We then iterate over the properties of the Meta Class to find the one whose name is equal to prop (for example "a").
- If that is the case we then use Spock's Mock/Spy method to create a Test Double of a given class (type).
- Finally we have to bind the mocked nested element to its parent.
- For the Spy it's easy since we set the value on the parent (lastObject = obj[prop]).
- For the mocks however we need to use the overloaded >> operator to record the behavior for our mock - that's why dynamically call the property that is present in the prop variable (lastObject."${prop}" >> mockedProp).
- Then we return from the closure the mocked/spied instance and we repeat the process for it
Class to be tested
Let's take a look at the class to be tested:PlayerServiceImpl.java
package com.blogspot.toomuchcoding.service;
import com.blogspot.toomuchcoding.model.PlayerDetails;
/**
* User: mgrzejszczak
* Date: 08.06.13
* Time: 19:02
*/
public class PlayerServiceImpl implements PlayerService {
@Override
public boolean isPlayerOfGivenCountry(PlayerDetails playerDetails, String country) {
String countryValue = playerDetails.getClubDetails().getCountry().getCountryCode().getCountryCode().value();
return countryValue.equalsIgnoreCase(country);
}
}
The test class
And now the test class:PlayerServiceImplWrittenUsingSpockTest.groovy
package com.blogspot.toomuchcoding.service
import com.blogspot.toomuchcoding.model.*
import com.blogspot.toomuchcoding.spock.ExtendedSpockSpecification
/**
* User: mgrzejszczak
* Date: 14.06.13
* Time: 16:06
*/
class PlayerServiceImplWrittenUsingSpockTest extends ExtendedSpockSpecification {
public static final String COUNTRY_CODE_ENG = "ENG";
PlayerServiceImpl objectUnderTest
def setup(){
objectUnderTest = new PlayerServiceImpl()
}
def "should return true if country code is the same when creating nested structures using groovy"() {
given:
PlayerDetails playerDetails = new PlayerDetails(
clubDetails: new ClubDetails(
country: new CountryDetails(
countryCode: new CountryCodeDetails(
countryCode: CountryCodeType.ENG
)
)
)
)
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}
def "should return true if country code is the same when creating nested structures using spock mocks - requires CGLIB for non interface types"() {
given:
PlayerDetails playerDetails = Mock()
ClubDetails clubDetails = Mock()
CountryDetails countryDetails = Mock()
CountryCodeDetails countryCodeDetails = Mock()
countryCodeDetails.countryCode >> CountryCodeType.ENG
countryDetails.countryCode >> countryCodeDetails
clubDetails.country >> countryDetails
playerDetails.clubDetails >> clubDetails
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}
def "should return true if country code is the same using ExtendedSpockSpecification's createNestedMocks"() {
given:
PlayerDetails playerDetails = Mock()
CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode >> CountryCodeType.ENG
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}
def "should return false if country code is not the same using ExtendedSpockSpecification createNestedMocks"() {
given:
PlayerDetails playerDetails = Mock()
CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode >> CountryCodeType.PL
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
!playerIsOfGivenCountry
}
def "should return true if country code is the same using ExtendedSpockSpecification's createNestedSpies"() {
given:
PlayerDetails playerDetails = Spy()
CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode = CountryCodeType.ENG
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}
def "should return false if country code is not the same using ExtendedSpockSpecification's createNestedSpies"() {
given:
PlayerDetails playerDetails = Spy()
CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode = CountryCodeType.PL
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
!playerIsOfGivenCountry
}
}
Let's move through the test methods one by one. First I present the code and then have a quick description of the presented snippet.
def "should return true if country code is the same when creating nested structures using groovy"() {
given:
PlayerDetails playerDetails = new PlayerDetails(
clubDetails: new ClubDetails(
country: new CountryDetails(
countryCode: new CountryCodeDetails(
countryCode: CountryCodeType.ENG
)
)
)
)
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}
Here you could find the approach of creating nested structures by using the Groovy feature of passing properties to be set in the constructor.
def "should return true if country code is the same when creating nested structures using spock mocks - requires CGLIB for non interface types"() {
given:
PlayerDetails playerDetails = Mock()
ClubDetails clubDetails = Mock()
CountryDetails countryDetails = Mock()
CountryCodeDetails countryCodeDetails = Mock()
countryCodeDetails.countryCode >> CountryCodeType.ENG
countryDetails.countryCode >> countryCodeDetails
clubDetails.country >> countryDetails
playerDetails.clubDetails >> clubDetails
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}
Here you can find a test that creates mocks using Spock - mind you that you need CGLIB as a dependency when you are mocking non interface types.
def "should return true if country code is the same using ExtendedSpockSpecification's createNestedMocks"() {
given:
PlayerDetails playerDetails = Mock()
CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode >> CountryCodeType.ENG
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}
Here you have an example of creating nested mocks using the createNestedMocks method.
def "should return false if country code is not the same using ExtendedSpockSpecification createNestedMocks"() {
given:
PlayerDetails playerDetails = Mock()
CountryCodeDetails countryCodeDetails = createNestedMocks(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode >> CountryCodeType.PL
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
!playerIsOfGivenCountry
}
An example showing that creating nested mocks using the createNestedMocks method really works - should return false for improper country code.
def "should return true if country code is the same using ExtendedSpockSpecification's createNestedSpies"() {
given:
PlayerDetails playerDetails = Spy()
CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode = CountryCodeType.ENG
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
playerIsOfGivenCountry
}
Here you have an example of creating nested spies using the createNestedSpies method.
def "should return false if country code is not the same using ExtendedSpockSpecification's createNestedSpies"() {
given:
PlayerDetails playerDetails = Spy()
CountryCodeDetails countryCodeDetails = createNestedSpies(playerDetails, "clubDetails.country.countryCode")
countryCodeDetails.countryCode = CountryCodeType.PL
when:
boolean playerIsOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
then:
!playerIsOfGivenCountry
}
An example showing that creating nested spies using the createNestedSpies method really works - should return false for improper country code.
Summary
In this post I have shown you how you can create nested mocks and spies using Spock. It can be useful especially when you are working with nested structures such as JAXB. Still you have to bear in mind that those structures to some extend violate the Law of Demeter. If you check my previous article about Mockito you would see that:We are getting the nested elements from the JAXB generated classes. Although it violates the Law of Demeter it is quite common to call methods of structures because JAXB generated classes are in fact structures so in fact I fully agree with Martin Fowler that it should be called the Suggestion of Demeter.And in case of this example the idea is the same - we are talking about structures so we don't violate the Law of Demeter.
Advantages
- With a single method you can mock/spy nested elements
- Code cleaner than creating tons of objects that you then have to manually set
Disadvantages
- Your IDE won't help you with providing the property names since the properties are presented as Strings
- You have to set Test Doubles only in the Specification context (and sometimes you want to go outside this scope)
Sources
As usual the sources are available at BitBucket and GitHub.The Essential Drools Cheat Sheet at DZone is out!
Hi! I'm pleased to announce that Maro Fusco's and mine DZone Refcard - The Essential Drools Cheat Sheet is out at https://refcardz.dzone.com/refcardz/drools. Hope you enjoy it :)
Mockito - Extra Interfaces with annotations and static methods
In the code I have quite recently came across a really bad piece of code that based on class casting in terms of performing some actions on objects. Of course the code needed to be refactored but sometimes you can't do it / or don't want to do it (and it should be understandable) if first you don't have unit tests of that functionality. In the following post I will show how to test such code, how to refactor it and in fact what I think about such code ;)
Let's take a look at the project structure:
As presented in the post regarding Mocktio RETURNS_DEEP_STUBS Answer for JAXB yet again we have the JAXB generated classes by the JAXB compiler in the com.blogspot.toomuchcoding.model package. Let's ommit the discussion over the pom.xml file since it's exactly the same as in the previous post.
In the com.blogspot.toomuchcoding.adapter package we have adapters over the JAXB PlayerDetails class that provides access to the Player interface. There is the
CommonPlayerAdapter.java
DefencePlayerAdapter.java
PlayerFactoryImpl.java
PlayerServiceImpl.java
Mockito comes to the rescue (although you shouldn't overuse this feature - in fact if you need to use it please consider refactoring it) with its extraInterfaces feature:
PlayerServiceImplTest.java
We start with providing the @RunWith(MockitoJUnitRunner.class) annotation which alows us to use the Mockito annotations such as @Mock and @InjectMocks.
Speaking of which @Mock annotation creates a Mock whereas @InjectMocks inject all the mocks either by constructor or by setters (that's awesome isn't it? :) ).
For the defensive player we are using the extra element of the annotation extraInterfaces that provides additional interfaces for the given Mock. You can also write (what you can find in the shouldReturnDefensivePlayerBeingADjAndAJavaDevByUsingWithSettings test) :
We are using the BDDMockito static methods like given(...).willReturn(...).willAnswer(...) etc. Then we are stubbing void methods with our custom Anwsers. In the next line you can see that in order to stub methods of another interface you have to cast the mock to the given interface. The same is related to the verification phase where i norder to check if a method was executed you have to cast the mock to the given interface.
You could improve the test by returning a real implementation from the factory or if it's a "heavy" operation to create one you could return a mock of such an implementation. What I wanted to show here is how to use the extra interfaces in Mockito (perhaps my usecase is not the best one ;) ). Anyway the implementation presented in the test is bad so we should think of the way to refactor it...
One of the ideas could be, assuming that the additional logic done in the Service is a part of the creation of the object, to move the code to the factory as such:
PlayFactoryImplWithFieldSettingLogic.java
PlayerServiceImplWIthoutUnnecessaryLogic.java
Summing it all up I hope that I managed to show how to:
The sources are available at the TooMuchCoding Bitbucket repository and TooMuchCoding Github repository.
In the com.blogspot.toomuchcoding.adapter package we have adapters over the JAXB PlayerDetails class that provides access to the Player interface. There is the
CommonPlayerAdapter.java
package com.blogspot.toomuchcoding.adapter;
import com.blogspot.toomuchcoding.model.Player;
import com.blogspot.toomuchcoding.model.PlayerDetails;
/**
* User: mgrzejszczak
* Date: 09.06.13
* Time: 15:42
*/
public class CommonPlayerAdapter implements Player {
private final PlayerDetails playerDetails;
public CommonPlayerAdapter(PlayerDetails playerDetails){
this.playerDetails = playerDetails;
}
@Override
public void run() {
System.out.printf("Run %s. Run!%n", playerDetails.getName());
}
public PlayerDetails getPlayerDetails() {
return playerDetails;
}
}
DefencePlayerAdapter.java
package com.blogspot.toomuchcoding.adapter;OffensivePlayerAdapter.java
import com.blogspot.toomuchcoding.model.DJ;
import com.blogspot.toomuchcoding.model.DefensivePlayer;
import com.blogspot.toomuchcoding.model.JavaDeveloper;
import com.blogspot.toomuchcoding.model.PlayerDetails;
/**
* User: mgrzejszczak
* Date: 09.06.13
* Time: 15:42
*/
public class DefencePlayerAdapter extends CommonPlayerAdapter implements DefensivePlayer, DJ, JavaDeveloper {
public DefencePlayerAdapter(PlayerDetails playerDetails){
super(playerDetails);
}
@Override
public void defend(){
System.out.printf("Defence! %s. Defence!%n", getPlayerDetails().getName());
}
@Override
public void playSomeMusic() {
System.out.println("Oops I did it again...!");
}
@Override
public void doSomeSeriousCoding() {
System.out.println("System.out.println(\"Hello world\");");
}
}
package com.blogspot.toomuchcoding.adapter;Ok, now let's go to the more interesting part. Let us assume that we have a very simple factory of players:
import com.blogspot.toomuchcoding.model.OffensivePlayer;
import com.blogspot.toomuchcoding.model.PlayerDetails;
/**
* User: mgrzejszczak
* Date: 09.06.13
* Time: 15:42
*/
public class OffensivePlayerAdapter extends CommonPlayerAdapter implements OffensivePlayer {
public OffensivePlayerAdapter(PlayerDetails playerDetails){
super(playerDetails);
}
@Override
public void shoot(){
System.out.printf("%s Shooooot!.%n", getPlayerDetails().getName());
}
}
PlayerFactoryImpl.java
package com.blogspot.toomuchcoding.factory;Ok so we have the factory that builds Players. Let's take a look at the Service that uses the factory:
import com.blogspot.toomuchcoding.adapter.CommonPlayerAdapter;
import com.blogspot.toomuchcoding.adapter.DefencePlayerAdapter;
import com.blogspot.toomuchcoding.adapter.OffensivePlayerAdapter;
import com.blogspot.toomuchcoding.model.Player;
import com.blogspot.toomuchcoding.model.PlayerDetails;
import com.blogspot.toomuchcoding.model.PositionType;
/**
* User: mgrzejszczak
* Date: 09.06.13
* Time: 15:53
*/
public class PlayerFactoryImpl implements PlayerFactory {
@Override
public Player createPlayer(PositionType positionType) {
PlayerDetails player = createCommonPlayer(positionType);
switch (positionType){
case ATT:
return new OffensivePlayerAdapter(player);
case MID:
return new OffensivePlayerAdapter(player);
case DEF:
return new DefencePlayerAdapter(player);
case GK:
return new DefencePlayerAdapter(player);
default:
return new CommonPlayerAdapter(player);
}
}
private PlayerDetails createCommonPlayer(PositionType positionType){
PlayerDetails playerDetails = new PlayerDetails();
playerDetails.setPosition(positionType);
return playerDetails;
}
}
PlayerServiceImpl.java
package com.blogspot.toomuchcoding.service;Let's admit it... this code is bad. Internally when you look at it (regardless of the fact whether it used instance of operator or not) you feel that it is evil :) As you can see in the code we have some class casts going on... How on earth can we test it? In the majority of testing frameworks you can't do such class casts on mocks since they are built with the CGLIB library and there can be some ClassCastExceptions thrown. You could still not return mocks and real implementations (assuming that those will not perform any ugly stuff in the construction process) and it could actually work but still - this is bad code :P
import com.blogspot.toomuchcoding.factory.PlayerFactory;
import com.blogspot.toomuchcoding.model.*;
/**
* User: mgrzejszczak
* Date: 08.06.13
* Time: 19:02
*/
public class PlayerServiceImpl implements PlayerService {
private PlayerFactory playerFactory;
@Override
public Player playAGameWithAPlayerOfPosition(PositionType positionType) {
Player player = playerFactory.createPlayer(positionType);
player.run();
performAdditionalActions(player);
return player;
}
private void performAdditionalActions(Player player) {
if(player instanceof OffensivePlayer){
OffensivePlayer offensivePlayer = (OffensivePlayer) player;
performAdditionalActionsForTheOffensivePlayer(offensivePlayer);
}else if(player instanceof DefensivePlayer){
DefensivePlayer defensivePlayer = (DefensivePlayer) player;
performAdditionalActionsForTheDefensivePlayer(defensivePlayer);
}
}
private void performAdditionalActionsForTheOffensivePlayer(OffensivePlayer offensivePlayer){
offensivePlayer.shoot();
}
private void performAdditionalActionsForTheDefensivePlayer(DefensivePlayer defensivePlayer){
defensivePlayer.defend();
try{
DJ dj = (DJ)defensivePlayer;
dj.playSomeMusic();
JavaDeveloper javaDeveloper = (JavaDeveloper)defensivePlayer;
javaDeveloper.doSomeSeriousCoding();
}catch(ClassCastException exception){
System.err.println("Sorry, I can't do more than just play football...");
}
}
public PlayerFactory getPlayerFactory() {
return playerFactory;
}
public void setPlayerFactory(PlayerFactory playerFactory) {
this.playerFactory = playerFactory;
}
}
Mockito comes to the rescue (although you shouldn't overuse this feature - in fact if you need to use it please consider refactoring it) with its extraInterfaces feature:
extraInterfaces
MockSettings extraInterfaces(java.lang.Class<?>... interfaces)
Specifies extra interfaces the mock should implement. Might be useful for legacy code or some corner cases. For background, see issue 51 hereThis mysterious feature should be used very occasionally. The object under test should know exactly its collaborators & dependencies. If you happen to use it often than please make sure you are really producing simple, clean & readable code.
Examples:
Foo foo = mock(Foo.class, withSettings().extraInterfaces(Bar.class, Baz.class));
//now, the mock implements extra interfaces, so following casting is possible:
Bar bar = (Bar) foo;
Baz baz = (Baz) foo;
Parameters:interfaces
- extra interfaces the should implement.
Returns:settings instance so that you can fluently specify other settings
PlayerServiceImplTest.java
package com.blogspot.toomuchcoding.service;There are quite a few tests here so let's take a look at the most interesting ones. But before we do it let's
import com.blogspot.toomuchcoding.factory.PlayerFactory;
import com.blogspot.toomuchcoding.model.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.*;
/**
* User: mgrzejszczak
* Date: 08.06.13
* Time: 19:26
*/
@RunWith(MockitoJUnitRunner.class)
public class PlayerServiceImplTest {
@Mock
PlayerFactory playerFactory;
@InjectMocks
PlayerServiceImpl objectUnderTest;
@Mock(extraInterfaces = {DJ.class, JavaDeveloper.class})
DefensivePlayer defensivePlayerWithDjAndJavaDevSkills;
@Mock
DefensivePlayer defensivePlayer;
@Mock
OffensivePlayer offensivePlayer;
@Mock
Player commonPlayer;
@Test
public void shouldReturnOffensivePlayerThatRan() throws Exception {
//given
given(playerFactory.createPlayer(PositionType.ATT)).willReturn(offensivePlayer);
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.ATT);
//then
assertThat(createdPlayer == offensivePlayer, is(true));
verify(offensivePlayer).run();
}
@Test
public void shouldReturnDefensivePlayerButHeWontBeADjNorAJavaDev() throws Exception {
//given
given(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayer);
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);
//then
assertThat(createdPlayer == defensivePlayer, is(true));
verify(defensivePlayer).run();
verify(defensivePlayer).defend();
verifyNoMoreInteractions(defensivePlayer);
}
@Test
public void shouldReturnDefensivePlayerBeingADjAndAJavaDev() throws Exception {
//given
given(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayerWithDjAndJavaDevSkills);
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
System.out.println("Hit me baby one more time!");
return null;
}
}).when(((DJ) defensivePlayerWithDjAndJavaDevSkills)).playSomeMusic();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
System.out.println("public static void main(String... args){\n}");
return null;
}
}).when(((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills)).doSomeSeriousCoding();
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);
//then
assertThat(createdPlayer == defensivePlayerWithDjAndJavaDevSkills, is(true));
verify(defensivePlayerWithDjAndJavaDevSkills).run();
verify(defensivePlayerWithDjAndJavaDevSkills).defend();
verify((DJ) defensivePlayerWithDjAndJavaDevSkills).playSomeMusic();
verify((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills).doSomeSeriousCoding();
}
@Test
public void shouldReturnDefensivePlayerBeingADjAndAJavaDevByUsingWithSettings() throws Exception {
//given
DefensivePlayer defensivePlayerWithDjAndJavaDevSkills = mock(DefensivePlayer.class, withSettings().extraInterfaces(DJ.class, JavaDeveloper.class));
given(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayerWithDjAndJavaDevSkills);
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
System.out.println("Hit me baby one more time!");
return null;
}
}).when(((DJ) defensivePlayerWithDjAndJavaDevSkills)).playSomeMusic();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
System.out.println("public static void main(String... args){\n}");
return null;
}
}).when(((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills)).doSomeSeriousCoding();
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);
//then
assertThat(createdPlayer == defensivePlayerWithDjAndJavaDevSkills, is(true));
verify(defensivePlayerWithDjAndJavaDevSkills).run();
verify(defensivePlayerWithDjAndJavaDevSkills).defend();
verify((DJ) defensivePlayerWithDjAndJavaDevSkills).playSomeMusic();
verify((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills).doSomeSeriousCoding();
}
@Test
public void shouldReturnCommonPlayer() throws Exception {
//given
given(playerFactory.createPlayer(null)).willReturn(commonPlayer);
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(null);
//then
assertThat(createdPlayer, is(commonPlayer));
}
}
We start with providing the @RunWith(MockitoJUnitRunner.class) annotation which alows us to use the Mockito annotations such as @Mock and @InjectMocks.
Speaking of which @Mock annotation creates a Mock whereas @InjectMocks inject all the mocks either by constructor or by setters (that's awesome isn't it? :) ).
For the defensive player we are using the extra element of the annotation extraInterfaces that provides additional interfaces for the given Mock. You can also write (what you can find in the shouldReturnDefensivePlayerBeingADjAndAJavaDevByUsingWithSettings test) :
DefensivePlayer defensivePlayerWithDjAndJavaDevSkills = mock(DefensivePlayer.class, withSettings().extraInterfaces(DJ.class, JavaDeveloper.class));Let's take a closer look at the test that we wrote for the functionality related to the DefensivePlayer and the casting part of the tested function:
@Test
public void shouldReturnDefensivePlayerBeingADjAndAJavaDev() throws Exception {
//given
given(playerFactory.createPlayer(PositionType.GK)).willReturn(defensivePlayerWithDjAndJavaDevSkills);
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
System.out.println("Hit me baby one more time!");
return null;
}
}).when(((DJ) defensivePlayerWithDjAndJavaDevSkills)).playSomeMusic();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
System.out.println("public static void main(String... args){\n}");
return null;
}
}).when(((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills)).doSomeSeriousCoding();
//when
Player createdPlayer = objectUnderTest.playAGameWithAPlayerOfPosition(PositionType.GK);
//then
assertThat(createdPlayer == defensivePlayerWithDjAndJavaDevSkills, is(true));
verify(defensivePlayerWithDjAndJavaDevSkills).run();
verify(defensivePlayerWithDjAndJavaDevSkills).defend();
verify((DJ) defensivePlayerWithDjAndJavaDevSkills).playSomeMusic();
verify((JavaDeveloper) defensivePlayerWithDjAndJavaDevSkills).doSomeSeriousCoding();
}
We are using the BDDMockito static methods like given(...).willReturn(...).willAnswer(...) etc. Then we are stubbing void methods with our custom Anwsers. In the next line you can see that in order to stub methods of another interface you have to cast the mock to the given interface. The same is related to the verification phase where i norder to check if a method was executed you have to cast the mock to the given interface.
You could improve the test by returning a real implementation from the factory or if it's a "heavy" operation to create one you could return a mock of such an implementation. What I wanted to show here is how to use the extra interfaces in Mockito (perhaps my usecase is not the best one ;) ). Anyway the implementation presented in the test is bad so we should think of the way to refactor it...
One of the ideas could be, assuming that the additional logic done in the Service is a part of the creation of the object, to move the code to the factory as such:
PlayFactoryImplWithFieldSettingLogic.java
package com.blogspot.toomuchcoding.factory;In this way there is no casting the code is really clean. And now the PlayerService looks like this:
import com.blogspot.toomuchcoding.adapter.CommonPlayerAdapter;
import com.blogspot.toomuchcoding.adapter.DefencePlayerAdapter;
import com.blogspot.toomuchcoding.adapter.OffensivePlayerAdapter;
import com.blogspot.toomuchcoding.model.*;
/**
* User: mgrzejszczak
* Date: 09.06.13
* Time: 15:53
*/
public class PlayerFactoryImplWithFieldSettingLogic implements PlayerFactory {
@Override
public Player createPlayer(PositionType positionType) {
PlayerDetails player = createCommonPlayer(positionType);
switch (positionType){
case ATT:
return createOffensivePlayer(player);
case MID:
return createOffensivePlayer(player);
case DEF:
return createDefensivePlayer(player);
case GK:
return createDefensivePlayer(player);
default:
return new CommonPlayerAdapter(player);
}
}
private Player createDefensivePlayer(PlayerDetails player) {
DefencePlayerAdapter defencePlayerAdapter = new DefencePlayerAdapter(player);
defencePlayerAdapter.defend();
defencePlayerAdapter.playSomeMusic();
defencePlayerAdapter.doSomeSeriousCoding();
return defencePlayerAdapter;
}
private OffensivePlayer createOffensivePlayer(PlayerDetails player) {
OffensivePlayer offensivePlayer = new OffensivePlayerAdapter(player);
offensivePlayer.shoot();
return offensivePlayer;
}
private PlayerDetails createCommonPlayer(PositionType positionType){
PlayerDetails playerDetails = new PlayerDetails();
playerDetails.setPosition(positionType);
return playerDetails;
}
}
PlayerServiceImplWIthoutUnnecessaryLogic.java
package com.blogspot.toomuchcoding.service;And the question arises whether there is even any need to have such a method in your code base...
import com.blogspot.toomuchcoding.factory.PlayerFactory;
import com.blogspot.toomuchcoding.model.*;
/**
* User: mgrzejszczak
* Date: 08.06.13
* Time: 19:02
*/
public class PlayerServiceImplWithoutUnnecessaryLogic implements PlayerService {
private PlayerFactory playerFactory;
/**
* What's the point in having this method then?
* @param positionType
* @return
*/
@Override
public Player playAGameWithAPlayerOfPosition(PositionType positionType) {
return playerFactory.createPlayer(positionType);
}
public PlayerFactory getPlayerFactory() {
return playerFactory;
}
public void setPlayerFactory(PlayerFactory playerFactory) {
this.playerFactory = playerFactory;
}
}
Summing it all up I hope that I managed to show how to:
- Use MockitoJUnitRunner to inject mocks in a clean way
- Use annotations or static methods to add extra interfaces that can be used by your mock
- Use BDDMockito to perform method stubbing
- Stub void methods with custom answer
- Stub and verify methods of the extra interfaces
- Refactor code that is using class casts
The sources are available at the TooMuchCoding Bitbucket repository and TooMuchCoding Github repository.
Mockito - RETURNS_DEEP_STUBS for JAXB
Sorry for not having written for some time but I was busy with writing the JBoss Drools Refcard for DZone and I am in the middle of writing a book about Mockito so I don't have too much time left for blogging...
Anyway quite recently on my current project I had an interesting situation regarding unit testing with Mockito and JAXB structures. We have very deeply nested JAXB structures generated from schemas that are provided for us which means that we can't change it in anyway.
Let's take a look at the project structure:
The project structure is pretty simple - there is a Player.xsd schema file that thanks to using the jaxb2-maven-plugin produces the generated JAXB Java classes corresponding to the schema in the target/jaxb/ folder in the appropriate package that is defined in the pom.xml. Speaking of which let's take a look at the pom.xml file.
The pom.xml :
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"Apart from the previously defined project dependencies, as mentioned previously in the jaxb2-maven-plugin in the configuration node you can define the packageName value that defines to which package should the JAXB classes be generated basing on the schemaDirectory value where the plugin can find the proper schema files.
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.blogspot.toomuchcoding</groupId>
<artifactId>mockito-deep_stubs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>spring-release</id>
<url>https://maven.springframework.org/release</url>
</repository>
<repository>
<id>maven-us-nuxeo</id>
<url>https://maven-us.nuxeo.org/nexus/content/groups/public</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<packageName>com.blogspot.toomuchcoding.model</packageName>
<schemaDirectory>${project.basedir}/src/main/resources/xsd</schemaDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>
Speaking of which let's check the Player.xsd schema file (simillar to the one that was present in the Spring JMS automatic message conversion article of mine):
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="https://www.w3.org/2001/XMLSchema">
<xsd:element name="PlayerDetails">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Name" type="xsd:string"/>
<xsd:element name="Surname" type="xsd:string"/>
<xsd:element name="Position" type="PositionType"/>
<xsd:element name="Age" type="xsd:int"/>
<xsd:element name="ClubDetails" type="ClubDetails"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="ClubDetails">
<xsd:sequence>
<xsd:element name="TeamName" type="xsd:string"/>
<xsd:element name="Country" type="CountryDetails"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CountryDetails">
<xsd:sequence>
<xsd:element name="CountryName" type="xsd:string"/>
<xsd:element name="CountryCode" type="CountryCodeDetails"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CountryCodeDetails">
<xsd:sequence>
<xsd:element name="CountryName" type="xsd:string"/>
<xsd:element name="CountryCode" type="CountryCodeType"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="CountryCodeType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="PL"/>
<xsd:enumeration value="GER"/>
<xsd:enumeration value="FRA"/>
<xsd:enumeration value="ENG"/>
<xsd:enumeration value="ESP"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="PositionType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="GK"/>
<xsd:enumeration value="DEF"/>
<xsd:enumeration value="MID"/>
<xsd:enumeration value="ATT"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
As you can see I'm defining some complex types that even though might have no business sense but you can find such examples in the real life :)
Let's find out how the method that we would like to test looks like. Here we have the PlayerServiceImpl that implements the PlayerService interface:
package com.blogspot.toomuchcoding.service;
import com.blogspot.toomuchcoding.model.PlayerDetails;
/**
* User: mgrzejszczak
* Date: 08.06.13
* Time: 19:02
*/
public class PlayerServiceImpl implements PlayerService {
@Override
public boolean isPlayerOfGivenCountry(PlayerDetails playerDetails, String country) {
String countryValue = playerDetails.getClubDetails().getCountry().getCountryCode().getCountryCode().value();
return countryValue.equalsIgnoreCase(country);
}
}
We are getting the nested elements from the JAXB generated classes. Although it violates the Law of Demeter it is quite common to call methods of structures because JAXB generated classes are in fact structures so in fact I fully agree with Martin Fowler that it should be called the Suggestion of Demeter. Anyway let's see how you could test the method:
@TestThe function checks if, once you have the same Country Code, you get a true boolean from the method. The only problem is the amount of sets and instantiations that take place when you want to create the input message. In our projects we have twice as many nested elements so you can only imagine the number of code that we would have to produce to create the input object...
public void shouldReturnTrueIfCountryCodeIsTheSame() throws Exception {
//given
PlayerDetails playerDetails = new PlayerDetails();
ClubDetails clubDetails = new ClubDetails();
CountryDetails countryDetails = new CountryDetails();
CountryCodeDetails countryCodeDetails = new CountryCodeDetails();
playerDetails.setClubDetails(clubDetails);
clubDetails.setCountry(countryDetails);
countryDetails.setCountryCode(countryCodeDetails);
countryCodeDetails.setCountryCode(CountryCodeType.ENG);
//when
boolean playerOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetails, COUNTRY_CODE_ENG);
//then
assertThat(playerOfGivenCountry, is(true));
}
So what can be done to improve this code? Mockito comes to the rescue to together with the RETURN_DEEP_STUBS default answer to the Mockito.mock(...) method:
@TestSo what happened here is that you use the Mockito.mock(...) method and provide the RETURNS_DEEP_STUBS answer that will create mocks automatically for you. Mind you that Enums can't be mocked that's why you can't write in the Mockito.when(...) function playerDetailsMock.getClubDetails().getCountry().getCountryCode().getCountryCode().getValue().
public void shouldReturnTrueIfCountryCodeIsTheSameUsingMockitoReturnDeepStubs() throws Exception {
//given
PlayerDetails playerDetailsMock = mock(PlayerDetails.class, RETURNS_DEEP_STUBS);
CountryCodeType countryCodeType = CountryCodeType.ENG;
when(playerDetailsMock.getClubDetails().getCountry().getCountryCode().getCountryCode()).thenReturn(countryCodeType);
//when
boolean playerOfGivenCountry = objectUnderTest.isPlayerOfGivenCountry(playerDetailsMock, COUNTRY_CODE_ENG);
//then
assertThat(playerOfGivenCountry, is(true));
}
Summing it up you can compare the readability of both tests and see how clearer it is to work with JAXB structures by using Mockito RETURNS_DEEP_STUBS default answer.
Naturally sources for this example are available at BitBucket and GitHub.
Execution of Groovy scripts from Java - XmlSlurper and MarkupBuilder in mapping issues
Problem with mappings
In our project we came across a really big problem related to mapping. Having two systems that initially were defined by the BA to be somehwat simillar we have chosen the simple XSLT (done through Altova Mapforce) of the input message to the output one.
Afterwards it turned out that the functions required to perform a mapping are becoming enormous. An example of such a mapping is:
From the input message take a list of Cars iterate over it and find a Car whose field "prodcutionDate" is the lowest and the attribute "make" is equal to "Honda" and as the output return the "saleDate"So in order to map it we decided to move to JBoss Drools. The decision tables were out of question since the logic was to complex and customized to be placed in the spreadsheet so we coded everything in the DRL files. Soon the rules got really big and some of our developers were forced to spend plenty of time on constant recreation of rules stated by the BA.
Out of frustration and after having seen all the amazing things at the 33rd degree conference I decided to start finding solutions to my problems which were:
- The DRL files are big and started to become unmaintainable (for a single field we had for example 4 rules)
- Since the BA has never coded a single Drools rule / XSLT in his life adding a simple if... else... statement for him is not a problem
- The BA has to wait for the mapping implementation by the devs until he can test it
- The devs are spending far too much time on coding the mapping rules instead of developing other features
After stating these problems a research regarding mapping frameworks took place and one of the concepts that I began working on was trying to create the mapping in Groovy. Since Groovy (thanks to for example PropertyMissing and MethodMissing) is a perfect language for creating a DSL I decided to start right away. The only two things I had to remember about were:
- The current application is written purely in Java
- The mapping code (in order to perform fast testing) has to be detached from the application as such - it can't be compiled during deployment because we want to have the possibility of frequent substitutions of the mappings
Project structure
Having defined the language, the constraints I created the following solution:
The project structure
As you can see the project structure is very simple. To begin with it is built in Gradle. The main function can be found in the XmlTransformer.java. The flow is such that the TransformerFactory creates a Transformer basing on the Groovy script that came out of the ScriptFactory (in our project for different types of products that we distinguish by a field in the XML file, we have different DRL files). The Groovy scripts are residing in the classpath in the /groovy/ folder (of course at the end of the day those scripts should be placed outside any jars).
In the build.gradle
apply plugin: 'java'
group = 'com.blogspot.toomuchcoding'
version = '1.0'
repositories {
mavenCentral()
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.0.5'
compile 'org.slf4j:slf4j-log4j12:1.7.2'
compile 'log4j:log4j:1.2.16'
compile 'com.google.guava:guava:14.0'
testCompile group: 'junit', name: 'junit', version: '4.+'
}
task(executeMain, dependsOn: 'classes', type: JavaExec) {
main = 'com.blogspot.toomuchcoding.XmlTransformer'
classpath = sourceSets.main.runtimeClasspath
}
we can see that there is no groovy plugin - it has been done deliberately since we don't want our scripts to be compiled. Now let's take a look at the logic behind the TransformerFactory that compiles the Groovy script. What is really important is the fact that our Groovy class implements an interface created in our Java project - we want from the Java point of view to have no problems with execution of the Groovy code.
TransformerFactoryImpl.java
package com.blogspot.toomuchcoding.factory;
import com.blogspot.toomuchcoding.transformer.Transformer;
import com.google.common.io.Resources;
import groovy.util.GroovyScriptEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.URL;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 22.03.13
* Time: 23:54
*/
public class TransformerFactoryImpl implements TransformerFactory<String, String> {
private static final String GROOVY_SCRIPTS_CLASSPATH = "groovy/";
private static Logger LOGGER = LoggerFactory.getLogger(TransformerFactoryImpl.class);
private ScriptFactory scriptFactory;
private GroovyScriptEngine groovyScriptEngine;
public TransformerFactoryImpl(ScriptFactory scriptFactory) {
this.scriptFactory = scriptFactory;
try {
groovyScriptEngine = new GroovyScriptEngine(GROOVY_SCRIPTS_CLASSPATH);
} catch (IOException e) {
LOGGER.error("Exception occurred while trying to create the Groovy script engine", e);
throw new RuntimeException(e);
}
}
@Override
public Transformer<String, String> createTransformer() {
Transformer<String, String> transformerFromScript = null;
try {
File scriptFile = scriptFactory.createScript();
URL scriptAsAClasspathResource = Resources.getResource(GROOVY_SCRIPTS_CLASSPATH + scriptFile.getName());
Class classFromScript = groovyScriptEngine.loadScriptByName(scriptAsAClasspathResource.getFile());
transformerFromScript = (Transformer<String, String>) classFromScript.newInstance();
} catch (Exception e) {
LOGGER.error("Exception occurred while trying to execute Groovy script", e);
}
return transformerFromScript;
}
}
A GroovyScriptEngine is used to load a script by name. I chose the GroovyScriptEngine (hopefully I used it in a good way ;) ) because:
The most complete solution for people who want to embed groovy scripts into their servers and have them reloaded on modification is the GroovyScriptEngine. You initialize the GroovyScriptEngine with a set of CLASSPATH like roots that can be URLs or directory names. You can then execute any Groovy script within those roots. The GSE will also track dependencies between scripts so that if any dependent script is modified the whole tree will be recompiled and reloaded.
I wanted to have some way of caching the compiled classes in order not to have any issues with PermGen.
Anyway you can see that I am doing some conversions to have the URL of the classpath Groovy script resource. At the end we are extracting a class from the Groovy script and we are casting it to the Transformer.
AbstractGroovyXmlTransformer.groovy
package groovy
import com.blogspot.toomuchcoding.transformer.Transformer
import groovy.util.slurpersupport.NodeChildren
import groovy.xml.MarkupBuilder
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 23.03.13
* Time: 02:16
*/
abstract class AbstractGroovyXmlTransformer implements Transformer<String, String> {
static Map<String, Object> MISSING_PROPERTIES = ["convertDate": new DateConverter(), "map": new Mapper()]
@Override
String transform(String input) {
def inputXml = new XmlSlurper().parseText input
def writer = new StringWriter()
def outputXml = new MarkupBuilder(writer)
doTransform inputXml, outputXml
writer.toString()
}
abstract void doTransform(inputXml, outputXml)
def propertyMissing(String name) {
Object property = MISSING_PROPERTIES[name]
assert property != null, "There is no function like [$name]. The ones that are supported are ${MISSING_PROPERTIES.keySet()}"
property
}
protected static class Mapper {
private Map<String, String> inputParameters
Mapper given(Map inputParameters) {
this.inputParameters = inputParameters
this
}
String from(NodeChildren nodeChildren) {
assert inputParameters != null, "The mapping can't be null!"
assert nodeChildren != null, "Node can't be null!"
String nodeText = nodeChildren.text()
String mappedValue = inputParameters[nodeText]
mappedValue ?: inputParameters.default
}
static Mapper map(Map<String, String> inputParameters) {
return new Mapper(inputParameters)
}
}
protected static class DateConverter {
private String inputDate
private String inputDateFormat
DateConverter from(NodeChildren nodeChildren) {
this.inputDate = nodeChildren.text()
this
}
DateConverter havingDateFormat(String inputDateFormat) {
this.inputDateFormat = inputDateFormat
this
}
String toOutputDateFormat(String outputDateFormat) {
assert inputDate != null, "The input date for which you are trying to do the conversion can't be null"
assert inputDateFormat != null, "The input date format for which you are trying to do the conversion can't be null"
assert outputDateFormat != null, "The output date format for which you are trying to do the conversion can't be null"
Date.parse(inputDateFormat, inputDate).format(outputDateFormat)
}
static DateConverter convertDate() {
new DateConverter()
}
}
}
In this abstract Groovy class I decided to place all the logic that could blur the image for the BA. In addition to that I created some helper classes and methods. In order to fully use the Groovy's DSL capabilities I used the propertyMissing method to map the words "map" and "convertDate" to create the instances of the helper classes which are used in the Builder design pattern way:
convertDate.from(inputXml.InputSystemContext.InputDate).havingDateFormat("dd/MM/yyyy").toOutputDateFormat("yy/MM/dd")
or
map.given("Some_action" : "Some_output_action", "default" : "something else").from(inputXml.AdditionalData.TypeOfAction)
If there is no such "function" (for example a BA makes a typo or sth) then an assertion error is being thrown and a list of supported "function" (which in reality are properties - but they are functions from the BA's perspective) is being printed.
Now let's move to the script that would be used by the BA.
GroovyXmlTransformer.groovy
package groovy
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 22.03.13
* Time: 23:59
*
* additional functions:
*
* convertDate.from(Node).havingDateFormat("DateFormat").toOutputDateFormat("AnotherDateFormat")
* map.given("Value to be mapped from" : "Value to be mapped to", "default" : "default value").from(Node)
*
*/
class GroovyXmlTransformer extends AbstractGroovyXmlTransformer {
@Override
void doTransform(inputXml, outputXml) {
outputXml.OutputSystemEnvelope() {
OutputSystemContext {
ResponseID(inputXml.InputSystemContext.RequestID.text().reverse())
OutputSource('OUTPUT_SYSTEM')
OutputDate(convertDate.from(inputXml.InputSystemContext.InputDate).havingDateFormat("dd/MM/yyyy").toOutputDateFormat("yy/MM/dd"))
}
OutputAdditionalData {
OutputReferenceNo("SOME_PREFIX_${inputXml.AdditionalData.ReferenceNo.text()}_SOME_SUFIX")
OutputTypeOfAction(map.given("Some_action" : "Some_output_action", "default" : "something else").from(inputXml.AdditionalData.TypeOfAction))
OutputTransactions {
inputXml.AdditionalData.Transactions.Transaction.each {
OutputTransaction(Client: it.Client, ProductType: it.ProductType, 'Done')
}
}
OutputProducts {
def minProduct = inputXml.AdditionalData.Products.Product.list().min { it.Value.text() }
def maxProduct = inputXml.AdditionalData.Products.Product.list().max { it.Value.text() }
MinProduct(name: minProduct.Name.text(), minProduct.Value.text())
MaxProduct(name: maxProduct.Name.text(), maxProduct.Value.text())
}
}
}
}
}
This piece of code does the following mapping (You can check the /xml/SampleXml.xml ):
Mapped from | Mapped to |
---|---|
InputSystemEnvelope | OutputSystemEnvelope |
InputSystemContex | OutputSystemContex |
RequestId | ResponseId (the Id should be reverted) |
InputSource | OutputSoutce (constant "UTPUT_SYSTEM") |
InputDate | OutputDate (converted from dd/MM/yyyy to yy/MM/dd) |
InputAdditionalData | OutputAdditionalData |
InputReferenceNo | OutputReferenceNo ( "SOME_PREFIX_" + value from InputReferenceNo + "_SOME_SUFIX") |
InputTypeOfAction | OutputTypeOfAction (mapped in such a way that if InputTypeOfAction is equal to "Some_action" then we will have "Some_output_action". Otherwise we get "something else") |
Transactions | OutputTransactions |
Transaction | OutputTransaction ( Attribute Client from Transaction.Client, Attribute ProductType from Transaction.ProductType, and the value "Done") |
Products | OutputProducts |
Product having min value | MinProduct |
Product having max value | MaxProduct |
The output
Converted from [<InputSystemEnvelope>
<InputSystemContext>
<RequestID>1234567890</RequestID>
<InputSource>INPUT_SYSTEM</InputSource>
<InputDate>22/03/2013</InputDate>
</InputSystemContext>
<AdditionalData>
<ReferenceNo>Ref1234567</ReferenceNo>
<TypeOfAction>Some_action</TypeOfAction>
<Transactions>
<Transaction>
<Client>ACME</Client>
<ProductType>IRS</ProductType>
</Transaction>
<Transaction>
<Client>Oracle</Client>
<ProductType>DB</ProductType>
</Transaction>
</Transactions>
<Products>
<Product>
<Name>Book</Name>
<Value>1</Value>
</Product>
<Product>
<Name>Car</Name>
<Value>10000</Value>
</Product>
<Product>
<Name>Boat</Name>
<Value>100000000</Value>
</Product>
<Product>
<Name>Spaceship</Name>
<Value>1000000000000000000</Value>
</Product>
</Products>
</AdditionalData>
</InputSystemEnvelope>]
to
[<OutputSystemEnvelope>
<OutputSystemContext>
<ResponseID>0987654321</ResponseID>
<OutputSource>OUTPUT_SYSTEM</OutputSource>
<OutputDate>13/03/22</OutputDate>
</OutputSystemContext>
<OutputAdditionalData>
<OutputReferenceNo>SOME_PREFIX_Ref1234567_SOME_SUFIX</OutputReferenceNo>
<OutputTypeOfAction>Some_output_action</OutputTypeOfAction>
<OutputTransactions>
<OutputTransaction Client='ACME' ProductType='IRS'>Done</OutputTransaction>
<OutputTransaction Client='Oracle' ProductType='DB'>Done</OutputTransaction>
</OutputTransactions>
<OutputProducts>
<MinProduct name='Book'>1</MinProduct>
<MaxProduct name='Spaceship'>1000000000000000000</MaxProduct>
</OutputProducts>
</OutputAdditionalData>
</OutputSystemEnvelope>]
Pros and cons
The pros and cons of this approach are as follows:Pros:
- The mapping is done sequentialy - field by field (it is easier to debug the problem)
- The mapping consists of vocabulary understandable by the BA
- Most of mappings could be done by the BA
- The majority of non-mapping grammar is hidden in the abstraction
- The compilation of the Groovy script is faster than creation of KnowledgeBases and compilation of Drools scripts
- Independence on the XML schema (each change of the schema would require the recompilation of the JAXB classes)
Cons:
- The BA would have to have some knowledge from the domain of computer science
- No parallel mapping
- The mapping might get less readable due to the fact that it is highly probable that the BA (out of lack of time) won't create a single function - all the logic will end up in the closures for a given Node.
- There might be some memory issues with parsing and recompilation of the Groovy scripts
- No XML schema may lead to improper output / input XML path setting
Summary
The problem with mapping that we encountered in our project turned out to be a very interesting issue to deal with. The example shown in this post is only a proposition of solving the issue and hopefully could be a starting point to a further discussion on the topic. If you have any ideas or opinions on this topic please leave a comment under this article.
The sources can be found on the Too Much Coding BitBucket repository and on GitHub.
Drools decision tables with Camel and Spring
Hi!
As I've shown it in my previous post JBoss Drools are a very useful rules engine. The only problem is that creating the rules in the Rule language might be pretty complicated for a non-technical person. That's why one can provide an easy way for creating business rules - decision tables created in a spreadsheet!
In the following example I will show you a really complicated business rule example converted to a decision table in a spreadsheet. As a backend we will have Drools, Camel and Spring.
To begin with let us take a look at our imaginary business problem. Let us assume that we are running a business that focuses on selling products (either Medical or Electronic). We are shipping our products to several countries (PL, USA, GER, SWE, UK, ESP) and depending on the country there are different law regulations concerning the buyer's age. In some countries you can buy products when you are younger than in others. What is more depending on the country from which the buyer and the product comes from and on the quantity of products, the buyer might get a discount. As you can see there is a substantial number of conditions needed to be fullfield in this scenario (imagine the number of ifs needed to program this :P ).
Another problem would be the business side (as usual). Anybody who has been working on a project knows how fast the requirements are changing. If one entered all the rules in the code he would have to redeploy the software each time the requirements changed. That's why it is a good practice to divide the business logic from the code itself. Anyway, let's go back to our example.
To begin with let us take a look at the spreadsheets (before that it is worth taking a look at the JBoss website with precise description of how the decision table should look like):
The point of entry of our program is the first spreadsheet that checks if the given user should be granted with the possibility of buying a product (it will be better if you download the spreadsheets and play with them from Too Much Coding's repository at Bitbucket: user_table.xls and product_table.xls, or Github user_table.xls and product_table.xls):
user_table.xls (tables worksheet)
Once the user has been approved he might get a discount:
product_table.xls (tables worksheet)
product_table.xls (lists worksheet)
As you can see in the images the business problem is quite complex. Each row represents a rule, and each column represents a condition. Do you remember the rules syntax from my recent post? So you would understand the hidden part of the spreadsheet that is right above the first visible row:
The rows from 2 to 6 represent some fixed configuration values such as rule set, imports ( you've already seen that in my recent post) and functions. Next in row number 7 you can find the name of the RuleTable. Then in row number 8 you have in our scenario either a CONDITION or an ACTION - so in other words either the LHS or rhe RHS respectively. Row number 9 is both representation of types presented in the condition and the binding to a variable. In row number 10 we have the exact LHS condition. Row number 11 shows the label of columns. From row number 12 we have the rules one by one. You can find the spreadsheets in the sources.
Now let's take a look at the code. Let's start with taking a look at the schemas defining the Product and the User.
Person.xsd
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="https://www.w3.org/2001/XMLSchema">
<xsd:include schemaLocation="user.xsd"/>
<xsd:element name="Product">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Name" type="xsd:string"/>
<xsd:element name="Type" type="ProductType"/>
<xsd:element name="Price" type="xsd:double"/>
<xsd:element name="CountryOfOrigin" type="CountryType"/>
<xsd:element name="AdditionalInfo" type="xsd:string"/>
<xsd:element name="Quantity" type="xsd:int"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:simpleType name="ProductType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="MEDICAL"/>
<xsd:enumeration value="ELECTRONIC"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
User.xsd
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="https://www.w3.org/2001/XMLSchema">
<xsd:include schemaLocation="product.xsd"/>
<xsd:element name="User">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="UserName" type="xsd:string"/>
<xsd:element name="UserAge" type="xsd:int"/>
<xsd:element name="UserCountry" type="CountryType"/>
<xsd:element name="Decision" type="DecisionType"/>
<xsd:element name="DecisionDescription" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:simpleType name="CountryType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="PL"/>
<xsd:enumeration value="USA"/>
<xsd:enumeration value="GER"/>
<xsd:enumeration value="SWE"/>
<xsd:enumeration value="UK"/>
<xsd:enumeration value="ESP"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="DecisionType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="ACCEPTED"/>
<xsd:enumeration value="REJECTED"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
Due to the fact that we are using maven we may use a plugin that will convert the XSD into Java classes.
part of the pom.xml
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<packageName>pl.grzejszczak.marcin.drools.decisiontable.model</packageName>
<schemaDirectory>${project.basedir}/src/main/resources/xsd</schemaDirectory>
</configuration>
</plugin>
</plugins>
</build>
Thanks to this plugin we have our generated by JAXB classes in the pl.grzejszczak.marcin.decisiontable.model package.
Now off to the drools-context.xml file where we've defined all the necessary beans as far as Drools are concerned:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:drools="https://drools.org/schema/drools-spring"
xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
https://drools.org/schema/drools-spring https://drools.org/schema/drools-spring.xsd">
<!-- Grid Node identifier that is registered in the CamelContext -->
<drools:grid-node id="node1"/>
<drools:kbase id="productsKBase" node="node1">
<drools:resources>
<drools:resource type="DTABLE" source="classpath:rules/product_table.xls"/>
</drools:resources>
</drools:kbase>
<drools:ksession id="productsKSession" name="productsKSession" type="stateless" kbase="productsKBase" node="node1"/>
<drools:kbase id="usersKBase" node="node1">
<drools:resources>
<drools:resource type="DTABLE" source="classpath:rules/user_table.xls"/>
</drools:resources>
</drools:kbase>
<drools:ksession id="usersKSession" name="usersKSession" type="stateless" kbase="usersKBase" node="node1"/>
</beans>
As you can see in comparison to the application context from the recent post there are some differences. First instead of passing the DRL file as the resource inside the knowledge base we are providing the Decision table (DTABLE). I've decided to pass in two seperate files but you can provide one file with several worksheets and access those worksheets (through the decisiontable-conf element). Also there is an additional element called node. We have to choose an implementation of the Node interface (Execution, Grid...) for the Camel route to work properly as you will see in a couple of seconds in the Spring application context file.
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:camel="https://camel.apache.org/schema/spring"
xmlns:context="https://www.springframework.org/schema/context"
xsi:schemaLocation="https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.0.xsd
https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
https://camel.apache.org/schema/spring https://camel.apache.org/schema/spring/camel-spring-2.8.0.xsd">
<import resource="classpath:drools-context.xml"/>
<!-- Show Spring where to search for the beans (in which packages) -->
<context:component-scan base-package="pl.grzejszczak.marcin.drools.decisiontable" />
<camel:camelContext id="camelContext">
<camel:route id="acceptanceRoute">
<camel:from uri="direct:acceptanceRoute"/>
<camel:to uri="drools:node1/usersKSession"/>
</camel:route>
<camel:route id="discountRoute">
<camel:from uri="direct:discountRoute"/>
<camel:to uri="drools:node1/productsKSession"/>
</camel:route>
</camel:camelContext>
</beans>
As you can see in order to access the Drools Camel Component we have to provide the node through which we will access the proper knowledge session. We have defined two routes - the first one ends at the Drools component that accesses the users knowledge session and the other the products knowledge session.
We have a ProductService interface implementation called ProductServiceImpl that given an input User and Product objects pass them through the Camel's Producer Template to two Camel routes each ending at the Drools components. The concept behind this product service is that we are first processing the User if he can even buy the software and then we are checking what kind of a discount he would receive. From the service's point of view in fact we are just sending the object out and waiting for the response. Finally having reveived the response we are passing the User and the Product to the Financial Service implementation that will bill the user for the products that he has bought or reject his offer if needed.
ProductServiceImpl.java
package pl.grzejszczak.marcin.drools.decisiontable.service;
import org.apache.camel.CamelContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pl.grzejszczak.marcin.drools.decisiontable.model.Product;
import pl.grzejszczak.marcin.drools.decisiontable.model.User;
import static com.google.common.collect.Lists.newArrayList;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 14.01.13
*/
@Component("productServiceImpl")
public class ProductServiceImpl implements ProductService {
private static final Logger LOGGER = LoggerFactory.getLogger(ProductServiceImpl.class);
@Autowired
CamelContext camelContext;
@Autowired
FinancialService financialService;
@Override
public void runProductLogic(User user, Product product) {
LOGGER.debug("Running product logic - first acceptance Route, then discount Route");
camelContext.createProducerTemplate().sendBody("direct:acceptanceRoute", newArrayList(user, product));
camelContext.createProducerTemplate().sendBody("direct:discountRoute", newArrayList(user, product));
financialService.processOrder(user, product);
}
}
ProductTypeConverter.java
package pl.grzejszczak.marcin.drools.decisiontable.converter;There is a good tutorial on TypeConverters on the Camel website - if you needed some more indepth info about it. Anyway, we are annotating our class and the functions used to convert different types into one another. What is important here is that we are showing Camel how to convert a list and a single product to Commands. Due to type erasure this will work regardless of the provided type that is why even though we are giving a list of Product and User, the toCommandFromList function will get executed.
import org.apache.camel.Converter;
import org.drools.command.Command;
import org.drools.command.CommandFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.grzejszczak.marcin.drools.decisiontable.model.Product;
import java.util.List;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 30.01.13
* Time: 21:42
*/
@Converter
public class ProductTypeConverter {
private static final Logger LOGGER = LoggerFactory.getLogger(ProductTypeConverter.class);
@Converter
public static Command toCommandFromList(List inputList) {
LOGGER.debug("Executing ProductTypeConverter's toCommandFromList method");
return CommandFactory.newInsertElements(inputList);
}
@Converter
public static Command toCommand(Product product) {
LOGGER.debug("Executing ProductTypeConverter's toCommand method");
return CommandFactory.newInsert(product);
}
}
In addition to this in order for the type converter to work we have to provide the fully quallified name of our class (FQN) in the /META-INF/services/org/apache/camel/TypeConverter file.
TypeConverter
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter
In order to properly test our functionality one should write quite a few tests that would verify the rules. A pretty good way would be to have input files stored in the test resources folders that are passed to the rule engine and then the result would be compared against the verified output (unfortunately it is rather impossible to make the business side develop such a reference set of outputs). Anyway let's take a look at the unit test that verifies only a few of the rules and the logs that are produced from running those rules:
ProductServiceImplTest.java
package pl.grzejszczak.marcin.drools.decisiontable.service.drools;Of course the log.debugs in the tests are totally redundant but I wanted you to quickly see that the rules are operational :) Sorry for the length of the logs but I wrote a few tests to show different combinations of rules (in fact it's better too have too many logs than the other way round :) )
import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import pl.grzejszczak.marcin.drools.decisiontable.model.*;
import pl.grzejszczak.marcin.drools.decisiontable.service.ProductService;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Created with IntelliJ IDEA.
* User: mgrzejszczak
* Date: 03.02.13
* Time: 16:06
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class ProductServiceImplTest {
private static final Logger LOGGER = LoggerFactory.getLogger(ProductServiceImplTest.class);
@Autowired
ProductService objectUnderTest;
@Test
public void testRunProductLogicUserPlUnderageElectronicCountryPL() throws Exception {
int initialPrice = 1000;
int userAge = 6;
int quantity = 10;
User user = createUser("Smith", CountryType.PL, userAge);
Product product = createProduct("Electronic", initialPrice, CountryType.PL, ProductType.ELECTRONIC, quantity);
printInputs(user, product);
objectUnderTest.runProductLogic(user, product);
printInputs(user, product);
assertTrue(product.getPrice() == initialPrice);
assertEquals(DecisionType.REJECTED, user.getDecision());
}
@Test
public void testRunProductLogicUserPlHighAgeElectronicCountryPLLowQuantity() throws Exception {
int initialPrice = 1000;
int userAge = 19;
int quantity = 1;
User user = createUser("Smith", CountryType.PL, userAge);
Product product = createProduct("Electronic", initialPrice, CountryType.PL, ProductType.ELECTRONIC, quantity);
printInputs(user, product);
objectUnderTest.runProductLogic(user, product);
printInputs(user, product);
assertTrue(product.getPrice() == initialPrice);
assertEquals(DecisionType.ACCEPTED, user.getDecision());
}
@Test
public void testRunProductLogicUserPlHighAgeElectronicCountryPLHighQuantity() throws Exception {
int initialPrice = 1000;
int userAge = 19;
int quantity = 8;
User user = createUser("Smith", CountryType.PL, userAge);
Product product = createProduct("Electronic", initialPrice, CountryType.PL, ProductType.ELECTRONIC, quantity);
printInputs(user, product);
objectUnderTest.runProductLogic(user, product);
printInputs(user, product);
double expectedDiscount = 0.1;
assertTrue(product.getPrice() == initialPrice * (1 - expectedDiscount));
assertEquals(DecisionType.ACCEPTED, user.getDecision());
}
@Test
public void testRunProductLogicUserUsaLowAgeElectronicCountryPLHighQuantity() throws Exception {
int initialPrice = 1000;
int userAge = 19;
int quantity = 8;
User user = createUser("Smith", CountryType.USA, userAge);
Product product = createProduct("Electronic", initialPrice, CountryType.PL, ProductType.ELECTRONIC, quantity);
printInputs(user, product);
objectUnderTest.runProductLogic(user, product);
printInputs(user, product);
assertTrue(product.getPrice() == initialPrice);
assertEquals(DecisionType.REJECTED, user.getDecision());
}
@Test
public void testRunProductLogicUserUsaHighAgeMedicalCountrySWELowQuantity() throws Exception {
int initialPrice = 1000;
int userAge = 22;
int quantity = 4;
User user = createUser("Smith", CountryType.USA, userAge);
Product product = createProduct("Some name", initialPrice, CountryType.SWE, ProductType.MEDICAL, quantity);
printInputs(user, product);
objectUnderTest.runProductLogic(user, product);
printInputs(user, product);
assertTrue(product.getPrice() == initialPrice);
assertEquals(DecisionType.ACCEPTED, user.getDecision());
}
@Test
public void testRunProductLogicUserUsaHighAgeMedicalCountrySWEHighQuantity() throws Exception {
int initialPrice = 1000;
int userAge = 22;
int quantity = 8;
User user = createUser("Smith", CountryType.USA, userAge);
Product product = createProduct("Some name", initialPrice, CountryType.SWE, ProductType.MEDICAL, quantity);
printInputs(user, product);
objectUnderTest.runProductLogic(user, product);
printInputs(user, product);
double expectedDiscount = 0.25;
assertTrue(product.getPrice() == initialPrice * (1 - expectedDiscount));
assertEquals(DecisionType.ACCEPTED, user.getDecision());
}
private void printInputs(User user, Product product) {
LOGGER.debug(ReflectionToStringBuilder.reflectionToString(user, ToStringStyle.MULTI_LINE_STYLE));
LOGGER.debug(ReflectionToStringBuilder.reflectionToString(product, ToStringStyle.MULTI_LINE_STYLE));
}
private User createUser(String name, CountryType countryType, int userAge){
User user = new User();
user.setUserName(name);
user.setUserCountry(countryType);
user.setUserAge(userAge);
return user;
}
private Product createProduct(String name, double price, CountryType countryOfOrigin, ProductType productType, int quantity){
Product product = new Product();
product.setPrice(price);
product.setCountryOfOrigin(countryOfOrigin);
product.setName(name);
product.setType(productType);
product.setQuantity(quantity);
return product;
}
}
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@1d48043[In this post I've presented how you can push some of your developing work to your BA by giving him a tool which he can be able to work woth - the Decision Tables in a spreadsheet. What is more now you will now how to integrate Drools with Camel. Hopefully you will see how you can simplify (thus minimize the cost of implementing and supporting) the implementation of business rules bearing in mind how prone to changes they are. I hope that this example will even better illustrate how difficult it would be to implement all the business rules in Java than in the previous post about Drools.
userName=Smith
userAge=6
userCountry=PL
decision=<null>
decisionDescription=<null>
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@1e8f2a0[
name=Electronic
type=ELECTRONIC
price=1000.0
countryOfOrigin=PL
additionalInfo=<null>
quantity=10
]
pl.grzejszczak.marcin.drools.decisiontable.service.ProductServiceImpl:31 Running product logic - first acceptance Route, then discount Route
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.ProductService:8 Sorry, according to your age (< 18) and country (PL) you can't buy this product
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.FinancialServiceImpl:29 Sorry, user has been rejected...
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@1d48043[
userName=Smith
userAge=6
userCountry=PL
decision=REJECTED
decisionDescription=Sorry, according to your age (< 18) and country (PL) you can't buy this product
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@1e8f2a0[
name=Electronic
type=ELECTRONIC
price=1000.0
countryOfOrigin=PL
additionalInfo=<null>
quantity=10
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@b28f30[
userName=Smith
userAge=19
userCountry=PL
decision=<null>
decisionDescription=<null>
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@d6a0e0[
name=Electronic
type=ELECTRONIC
price=1000.0
countryOfOrigin=PL
additionalInfo=<null>
quantity=1
]
pl.grzejszczak.marcin.drools.decisiontable.service.ProductServiceImpl:31 Running product logic - first acceptance Route, then discount Route
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.ProductService:8 Congratulations, you have successfully bought the product
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.ProductService:8 Sorry, no discount will be granted.
pl.grzejszczak.marcin.drools.decisiontable.service.FinancialServiceImpl:25 User has been approved - processing the order...
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@b28f30[
userName=Smith
userAge=19
userCountry=PL
decision=ACCEPTED
decisionDescription=Congratulations, you have successfully bought the product
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@d6a0e0[
name=Electronic
type=ELECTRONIC
price=1000.0
countryOfOrigin=PL
additionalInfo=Sorry, no discount will be granted.
quantity=1
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@14510ac[
userName=Smith
userAge=19
userCountry=PL
decision=<null>
decisionDescription=<null>
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@1499616[
name=Electronic
type=ELECTRONIC
price=1000.0
countryOfOrigin=PL
additionalInfo=<null>
quantity=8
]
pl.grzejszczak.marcin.drools.decisiontable.service.ProductServiceImpl:31 Running product logic - first acceptance Route, then discount Route
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.ProductService:8 Congratulations, you have successfully bought the product
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.ProductService:8 Congratulations - you've been granted a 10% discount!
pl.grzejszczak.marcin.drools.decisiontable.service.FinancialServiceImpl:25 User has been approved - processing the order...
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@14510ac[
userName=Smith
userAge=19
userCountry=PL
decision=ACCEPTED
decisionDescription=Congratulations, you have successfully bought the product
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@1499616[
name=Electronic
type=ELECTRONIC
price=900.0
countryOfOrigin=PL
additionalInfo=Congratulations - you've been granted a 10% discount!
quantity=8
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@17667bd[
userName=Smith
userAge=19
userCountry=USA
decision=<null>
decisionDescription=<null>
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@ad9f5d[
name=Electronic
type=ELECTRONIC
price=1000.0
countryOfOrigin=PL
additionalInfo=<null>
quantity=8
]
pl.grzejszczak.marcin.drools.decisiontable.service.ProductServiceImpl:31 Running product logic - first acceptance Route, then discount Route
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.ProductService:8 Sorry, according to your age (< 18) and country (USA) you can't buy this product
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.FinancialServiceImpl:29 Sorry, user has been rejected...
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@17667bd[
userName=Smith
userAge=19
userCountry=USA
decision=REJECTED
decisionDescription=Sorry, according to your age (< 18) and country (USA) you can't buy this product
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@ad9f5d[
name=Electronic
type=ELECTRONIC
price=1000.0
countryOfOrigin=PL
additionalInfo=<null>
quantity=8
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@9ff588[
userName=Smith
userAge=22
userCountry=USA
decision=<null>
decisionDescription=<null>
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@1b0d2d0[
name=Some name
type=MEDICAL
price=1000.0
countryOfOrigin=SWE
additionalInfo=<null>
quantity=4
]
pl.grzejszczak.marcin.drools.decisiontable.service.ProductServiceImpl:31 Running product logic - first acceptance Route, then discount Route
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.ProductService:8 Congratulations, you have successfully bought the product
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.FinancialServiceImpl:25 User has been approved - processing the order...
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@9ff588[
userName=Smith
userAge=22
userCountry=USA
decision=ACCEPTED
decisionDescription=Congratulations, you have successfully bought the product
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@1b0d2d0[
name=Some name
type=MEDICAL
price=1000.0
countryOfOrigin=SWE
additionalInfo=<null>
quantity=4
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@1b27882[
userName=Smith
userAge=22
userCountry=USA
decision=<null>
decisionDescription=<null>
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@5b84b[
name=Some name
type=MEDICAL
price=1000.0
countryOfOrigin=SWE
additionalInfo=<null>
quantity=8
]
pl.grzejszczak.marcin.drools.decisiontable.service.ProductServiceImpl:31 Running product logic - first acceptance Route, then discount Route
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.ProductService:8 Congratulations, you have successfully bought the product
pl.grzejszczak.marcin.drools.decisiontable.converter.ProductTypeConverter:25 Executing ProductTypeConverter's toCommandFromList method
pl.grzejszczak.marcin.drools.decisiontable.service.ProductService:8 Congratulations, you are granted a discount
pl.grzejszczak.marcin.drools.decisiontable.service.FinancialServiceImpl:25 User has been approved - processing the order...
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:150 pl.grzejszczak.marcin.drools.decisiontable.model.User@1b27882[
userName=Smith
userAge=22
userCountry=USA
decision=ACCEPTED
decisionDescription=Congratulations, you have successfully bought the product
]
pl.grzejszczak.marcin.drools.decisiontable.service.drools.ProductServiceImplTest:151 pl.grzejszczak.marcin.drools.decisiontable.model.Product@5b84b[
name=Some name
type=MEDICAL
price=750.0
countryOfOrigin=SWE
additionalInfo=Congratulations, you are granted a discount
quantity=8
]
If you have any experience with Drools in terms of decision tables, integration with Spring and Camel please feel free to leave a comment - let's have a discussion on that :)
All the code is available at Too Much Coding repository at Bitbucket and GitHub.
Cheers!
Concurrency in web applications
Nice article about concurrency in web applications (in Polish)
Drools - Rete Alogrithm explanation
Quite recently I have found a very nice article about the Rete Algorithm.
New Too Much Coding repository!
Hi!
As you know there is a code repository of Too Much Coding sources here at Bitbucket. If you don't like it you can also find the Too Much Coding sources at GitHub!
Enjoy :)
As you know there is a code repository of Too Much Coding sources here at Bitbucket. If you don't like it you can also find the Too Much Coding sources at GitHub!
Enjoy :)