Friday, August 13, 2010

Test drive with Arquillian and CDI (Part 2)

The first part of the Arquillian series was mainly focused on working with an in-memory database, DI (dependency injection) and events from the CDI spec. Now we will take a closer look on how to deal with testing Contextual components. For this purpose we will extend our sample project from the first part by adding a PortfolioController class, a conversation scoped bean for handling processing of user's portfolio management.

@ConversationScoped @Named("portfolioController")
public class PortfolioController implements Serializable {

// ...

Map<Share, Integer> sharesToBuy = new HashMap<Share, Integer>();

@Inject @LoggedIn
User user;

@Inject
private TradeService tradeService;

@Inject
private Conversation conversation;

public void buy(Share share, Integer amount) {
if (conversation.isTransient()) {
conversation.begin();
}
Integer currentAmount = sharesToBuy.get(share);
if (null == currentAmount) {
currentAmount = Integer.valueOf(0);
}

sharesToBuy.put(share, currentAmount + amount);
}

public void confirm() {
for (Map.Entry<Share, Integer> sharesAmount : sharesToBuy.entrySet()) {
tradeService.buy(user, sharesAmount.getKey(), sharesAmount.getValue());
}
conversation.end();
}

public void cancel() {
sharesToBuy.clear();
conversation.end();
}

// ...

}

So, let's try out Arquillian! As we already know from the first part we need to create a deployment package, which then will be deployed by Arquillian on the target container (in our case Glassfish 3.0.1 Embedded).


@Deployment
public static Archive<?> createDeploymentPackage() {
return ShrinkWrap.create("test.jar", JavaArchive.class)
.addPackages(false, Share.class.getPackage(),
ShareEvent.class.getPackage())
.addClasses(TradeTransactionDao.class,
ShareDao.class,
PortfolioController.class)
.addManifestResource(new ByteArrayAsset("<beans />".getBytes()), ArchivePaths.create("beans.xml"))
.addManifestResource("inmemory-test-persistence.xml", ArchivePaths.create("persistence.xml"));
}

Next we can start develop a simple test scenario:


  • given user choose CTP share,

  • when he confirms the order,

  • then his portfolio should be updated.

Which in JUnit realms could be written as follows:


@RunWith(Arquillian.class)
public class PortfolioControllerTest {

// deployment method

@Inject
ShareDao shareDao;

@Inject
PortfolioController portfolioController;

@Test
public void shouldAddCtpShareToUserPortfolio() {
// given
User user = portfolioController.getUser();
Share ctpShare = shareDao.getByKey("CTP");

// when
portfolioController.buy(ctpShare, 1);
portfolioController.confirm();

// then
assertThat(user.getSharesAmount(ctpShare)).isEqualTo(3);
}

}

Looks really simple, doesn't it? Well, it's almost that simple but there are some small details which you need to be aware of.

Producers

CDI provides a feature similar to Seam factories or Guice providers. It's called producer and it allows you to create injectable dependency. This could be especially useful when creation of such an instance requires additional logic, i.e. it needs to be obtained from an external source. A logged in user in a web application is a good example here. Thanks to the CDI @Produces construct we can still have very clean code which just works! All we need to do in order to inject the currently logged in user to our bean is as simple as that:

1. Create a @LoggedIn qualifier which will be used to define that a particular injection is expecting this concrete User bean.


@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface LoggedIn {
}

2. Implement the producer method which will instantiate the logged in user in the session scope just after he successfully accesses the application, so it will provide an instance of the User class which is of @LoggedIn "type".


@Produces @SessionScoped @LoggedIn User loggedInUser() {
// code for retrieving current user from session
}

3. Decorate all injection points in other beans where we need this instance.


@Inject @LoggedIn
User user;

However this construct could be problematic when writing tests and an attentive reader would probably already be concerned about it. But with Arquillian we will run our test code in the CDI container and there is no need to simulate login procedure, using mock http sessions or any other constructs. We can take full advantage of this fact and create producer method which will replace our original one and provide the user directly from entity manager for example.


@Produces @LoggedIn User loggedInUser() {
return entityManager.find(User.class, 1L);
}

Note: I removed @SessionScoped annotation from loggedInUser() producer method intentionally. Otherwise you could have troubles with Weld proxies and EclipseLink while trying to persist the entity class. For tests it actually does not make any difference.

Context handling

One small problem arrived when I tried to test logic based on the conversation context. I had to figure out a way to programmatically create the appropriate context which then will be used by the SUT (or CUT if you prefer this abbreviation), because I was getting org.jboss.weld.context.ContextNotActiveException. Unfortunately I wasn't able to find anything related to it on the Arquillian forum or wiki, so I desperately jumped to the Seam 3 examples. I read somewhere that they are also using this library to test their modules and sample projects. Bingo! I found what I was looking for. To make the test code more elegant I built my solution the same way as for handling the database in the first part - by using annotations and JUnit rules. Using a @RequiredScope annotation on the test method will instruct JUnit rule to handle proper context initialization and cleanup after finishing the test. To make the code even cleaner we can implement such logic in a dedicated class and treat the enum as a factory:


public enum ScopeType {

CONVERSATION {
@Override
public ScopeHandler getHandler() {
return new ConversationScopeHandler();
}
}

// ... other scopes

public abstract ScopeHandler getHandler();

}

public class ConversationScopeHandler implements ScopeHandler {

@Override
public void initializeContext() {
ConversationContext conversationContext = Container.instance().services().get(ContextLifecycle.class).getConversationContext();
conversationContext.setBeanStore(new HashMapBeanStore());
conversationContext.setActive(true);
}

@Override
public void cleanupContext() {
ConversationContext conversationContext = Container.instance().services().get(ContextLifecycle.class).getConversationContext();
if (conversationContext.isActive()) {
conversationContext.setActive(false);
conversationContext.cleanup();
}
}
}

The JUnit rule will only extract the annotation's value of the test method and delegate context handling to the proper implementation:


public class ScopeHandlingRule extends TestWatchman {

private ScopeHandler handler;

@Override
public void starting(FrameworkMethod method) {
RequiredScope rc = method.getAnnotation(RequiredScope.class);
if (null == rc) {
return;
}
ScopeType scopeType = rc.value();
handler = scopeType.getHandler();
handler.initializeContext();
}

@Override
public void finished(FrameworkMethod method) {
if (null != handler) {
handler.cleanupContext();
}
}
}

Finally here's fully working test class with two additional test scenarios. I also used DBUnit add-on from first post for convenience.


@RunWith(Arquillian.class)
public class PortfolioControllerTest {

@Rule
public DataHandlingRule dataHandlingRule = new DataHandlingRule();

@Rule
public ScopeHandlingRule scopeHandlingRule = new ScopeHandlingRule();

@Deployment
public static Archive<?> createDeploymentPackage() {
return ShrinkWrap.create("test.jar", JavaArchive.class)
.addPackages(false, Share.class.getPackage(),
ShareEvent.class.getPackage())
.addClasses(TradeTransactionDao.class,
ShareDao.class,
TradeService.class,
PortfolioController.class)
.addManifestResource(new ByteArrayAsset("<beans />".getBytes()), ArchivePaths.create("beans.xml"))
.addManifestResource("inmemory-test-persistence.xml", ArchivePaths.create("persistence.xml"));
}

@PersistenceContext
EntityManager entityManager;

@Inject
ShareDao shareDao;

@Inject
TradeTransactionDao tradeTransactionDao;

@Inject
PortfolioController portfolioController;

@Test
@PrepareData("datasets/shares.xml")
@RequiredScope(ScopeType.CONVERSATION)
public void shouldAddCtpShareToUserPortfolio() {
// given
User user = portfolioController.getUser();
Share ctpShare = shareDao.getByKey("CTP");

// when
portfolioController.buy(ctpShare, 1);
portfolioController.confirm();

// then
assertThat(user.getSharesAmount(ctpShare)).isEqualTo(3);
}

@Test
@PrepareData("datasets/shares.xml")
@RequiredScope(ScopeType.CONVERSATION)
public void shouldNotModifyUserPortfolioWhenCancelProcess() {
// given
User user = portfolioController.getUser();
Share ctpShare = shareDao.getByKey("CTP");

// when
portfolioController.buy(ctpShare, 1);
portfolioController.cancel();

// then
assertThat(user.getSharesAmount(ctpShare)).isEqualTo(2);
}

@Test
@RequiredScope(ScopeType.CONVERSATION)
@PrepareData("datasets/shares.xml")
public void shouldRecordTransactionWhenUserBuysAShare() {
// given
User user = portfolioController.getUser();
Share ctpShare = shareDao.getByKey("CTP");

// when
portfolioController.buy(ctpShare, 1);
portfolioController.confirm();

// then
List<TradeTransaction> transactions = tradeTransactionDao.getTransactions(user);
assertThat(transactions).hasSize(1);
}

@Produces @LoggedIn User loggedInUser() {
return entityManager.find(User.class, 1L);
}

}

For the full source code you can jump directly to our google code repository.

Conclusion

As you can see playing with Arquillian is pure fun for me. Latest 1.0.0.Alpha3 release brought a lot of new goodies to the table. I hope that the examples in this blog post convinced you that working with different scopes is quite straightforward and requires just a little bit of additional code. However it's still not the ideal solution because it's using Weld's internal API to create and manage scopes. So if you are using a different CDI container you need to figure out how to achieve it, but it's just a matter of adjusting ScopeHandler implementation to your needs.

There is much more to write about Arquillian so keep an eye on our blog and share your thoughts and suggestions through comments.

3 comments:

Anonymous said...

Glad to see you're getting some good mileage out of Arquillian and ShrinkWrap; welcome to the community.

S,
ALR

ابن فلسطين said...

it would be very interesting for me to see how could I test a webservice.
is there any deference, i mean to what you have described here?

Unknown said...

Could you oupdate your example to newer Weld version? The API has significanlty changed.