The key to successful unit testing is isolation!
A couple of years ago, when Spring framework entered the scene, we had some issues with unit testing in my team. Since Spring is based on IOC we felt we had no control of the code since just instancing a class and run tests on it would generate a lot of null pointers.
So we came up with the, what we thought back then, brilliant idea to wire up Spring for every unit test. This way we knew that all Spring beans would be set on the class to be tested and, hence, we got rid of the malicious null pointers.
But this introduced another problem, namely that if we have a service bean that utilizes a DAO bean we would have to make sure that the DAO bean being used would have a database to run on. Enter in-memory databases!
So now we had a test suite that could be run and no framework errors would prevent us from implementing unit tests for the code.
However, we did not realize that we were actually testing more than we wanted to!
In reality the tests we wrote for our service layer would also test the DAO layer.
Not really an ideal situation...
So what to do about this then? Spring is being used as defacto standard these days.
Enter Mockito :-)
With Mockito you have a tool that enables you to unit test exactly what you want.
Let me give you a little real-world example that I ran into the other day.
I am working with the customer system at my current employer and for each change it is important to track these changes with an audit trail. So, for example, if a customer logs into the system correctly an audit trail should be created with information about status (success) and some other information. Should the login fail then the same information apart from status = access_denied should be generated.
The audit trail is created with Spring's JmsTemplate and ActiveMQ's vm://localhost functionality (no socket connections involved).
So the code I want to test looks something like this (names etc. has been changed to protect my customer's code):
package com.x.domain.service;
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.util.Assert;import com.x.domain.Authentication;import com.x.domain.AuthenticationStatus;import com.x.domain.LoginDetails;import com.x.domain.infrastructure.messaging.AuditLoggingProducer;@Service("authenticationService")public class AuthenticationServiceImpl implements AuthenticationService {@Autowiredprivate AuditLoggingProducer auditLoggingProducer;public Authentication login(LoginDetails loginDetails) {Assert.notNull(loginDetails, "LoginDetails cannot be null");// Simplified code - used for illustration of this exampleAuthentication authentication = null;if (loginDetails.getUserId() == 1) {// Allow user with id to login - every one else should be denied// The winner (pos 1) takes it all, huh? :-)authentication = createAuthentication(AuthenticationStatus.SUCCESS, loginDetails.getUserId());} else {authentication = createAuthentication(AuthenticationStatus.FAILURE, loginDetails.getUserId());}return authentication;}/*** Creates authentication and makes sure that auditing service is called*/private Authentication createAuthentication(AuthenticationStatus status, int userId) {Authentication authentication = new Authentication(status, userId);auditLoggingProducer.audit(authentication);return authentication;}}
Now, in order to test the code above I will use some nice features of Mockito and Spring:
package com.x.domain.service;import org.mockito.ArgumentCaptor;import org.springframework.test.util.ReflectionTestUtils;import static org.mockito.Mockito.*;import static org.junit.Assert.*;import com.x.domain.Authentication;import com.x.domain.LoginDetails;import com.x.domain.infrastructure.messaging.AuditLoggingProducer;public class AuthenticationServiceTest {private ArgumentCaptor<Athentication> argument = ArgumentCaptor.forClass(Authentication.class);private AuthenticationServiceImpl authenticationServiceImpl;private AuditLoggerProducer auditLoggerProducer;@Beforepublic void setUp() {// Just create a simple instanceauthenticationServiceImpl = new AuthenticationServiceImpl();// Now wire it up with a mock (to prevent null pointer exception)auditLoggingProducer = mock(AuditLoggingProducer.class);ReflectionTestUtils.setField(authenticationServiceImpl, "auditLoggingProducer", auditLoggingProducer);}@Testpublic loginInWithValidUserShouldCreateSuccess() {LoginDetails loginDetails = new LoginDetails(1, ...);// Login with valid userAuthentication authentication = authenticationServiceImpl.login(loginDetails);// Assert that the returned authentication is of correct statusassertEquals(AuthenticationStatus.SUCCESS, authentication.getStatus());// Here comes the tricky bit to test:// Make sure that audit logging has been called (1 time), but// also make sure that the sent message is of status successverify(auditLoggingProducer, times(1)).audit(argument.capture());assertEquals(AuthenticationStatus.SUCCESS, argument.getValue().getStatus());}@Testpublic loginInWithInvalidUserShouldCreateFailure() {LoginDetails loginDetails = new LoginDetails(123, ...);// Login with valid userAuthentication authentication = authenticationServiceImpl.login(loginDetails);// Assert that the returned authentication is of correct statusassertEquals(AuthenticationStatus.FAILURE, authentication.getStatus());// Here comes the tricky bit to test:// Make sure that audit logging has been called (1 time), but// also make sure that the sent message is of status failureverify(auditLoggingProducer, times(1)).audit(argument.capture());assertEquals(AuthenticationStatus.FAILURE, argument.getValue().getStatus());}}
By utilizing the tools Mockito brings you can isolate you unit tests 100% (or close to) and therefore do proper unit testing!
Best of luck.
2 comments:
Nice tip :) I hadn't used the ReflectionTestUtils.
I wonder what that project was :) ?
Post a Comment