MockEJB Documentation
This is the documentation for MockEJB 0.5. APIs and capabilities of MockEJB 0.6
have changed pretty drastically, especially in regard to interceptors implementation.
This documentation will be updated soon.
Overview of MockEJB
JNDI
Support
How to Run MockEJB Tests Inside the
Container
Developing
and Deploying Mock EJBs
Support for Interceptors
Using Transactions with MockEJB
JMS
Support
Please start by reviewing MockEJB examples.
Top
MockEJB uses dynamic proxies to create objects
implementing Home and business (remote or local) interfaces. When the
client calls
MockContainer.deploy() , MockEJB creates
dynamic
proxy implementing the Home interface and binds it to the JNDI context
using Context.rebind() call. MockEJB also creates an
instance of the MockEJbObject
class
and returns it to the client.
Client can then look up the Home proxy in JNDI and call create() .
At this point, MockEJB creates dynamic proxy to the business interface
and returns it to the client.
Every call to the Home or business interface method goes
through the list (stack)
of interceptors. Clients have full control
over this call chain through the MockEjbObject instance
returned by the MockContainer.deploy() , e. g., they can
add or delete the interceptors
or change their behavior.
Top
MockEJB comes with in-memory JNDI implementation
provided by MockContext
and MockContextFactory
classes. MockContext is ideal for testing since it does
not persist objects. It means that any change to JNDI disappears after
the test case completes. You can also plug in a different JNDI
provider, for example you can use JNDI provider of your application
server.
In order to use MockContext , you need to
set org.mockejb.jndi.MockContextFactory
as the initial context factory. The easiest way to do it is to invoke MockContextFactory.setAsInitial()
method at the beginning of your test (in setUp ).
This method sets Context.INITIAL_CONTEXT_FACTORY and Context.URL_PKG_PREFIXES
system properties. The second property is required to be able to handle
"java:comp" context. More details on the URL-prefixed contexts can be
found
here.
Note that MockContext does not scope
"java:comp/env" context to the bean where
it was created. In other words, you can bind
any object to this context, however its name will be global, therefore
it should be unique within your test.
Since MockContextFactory.setAsInitial()
changes system properties, it is a good idea to restore the original
state of the environment in tearDown() . All you need to
do is to call MockContextFactory.revertSetAsInitial() .
This will allow
your test class to run inside the container without affecting test
classes that might potentially run after it. Otherwise, you'll need to
bounce the JVM of the application server.
Before you can run your test class, you have to populate
its JNDI tree.
To do this, you need to figure out what objects your EJBs (or any other
code that is
part of the test case) expect to see in JNDI. Then instantiate and bind
these objects.
For example, if your EJB needs JDBC support, you can instantiate and
bind the DataSource implementation that comes with your
JDBC driver. If you put this logic in the setUp method of
your class, you'll have to use Context.rebind() method,
since setUp will run multiple times per test class.
MockContext is capable of looking up
objects in a delegate JNDI context.
This means that MockContext first searches its local JNDI
tree and if the object is not found there, it will look in the JNDI
tree of the delegate context.
This is convenient when you want to rely on the resources deployed into
the application
server, such as data sources and connection factories. So, in this
case, application server acts as a delegate context. This approach
gives you the alternative to manually pre-populating MockContext
with all required resources.
Note that while using delegate context increases the
dependency of the test class on the external resource, such as the
application server, it preserves most of the benefits of the mock
objects approach. You can still run your EJBs outside of the
container and avoid having to redeploy them every time you change the
code. Application server is this case can be viewed as yet another
service provided by the OS
where your test class runs. To support this notion, you need to make
sure that the application server always runs in the background, e.g.,
you can start it as part of the startup sequence when you login.
Another important delegate context usage is the support
for mock EJBs when you run your tests inside the container. MockEJB
framework deploys mock EJBs using MockContext , whereas
all tested (non-mock) EJBs are bound in the JNDI tree of the
application server (deployed by
the regular means). See "Mock EJBs" section for
more details.
In order to setup the delegate context, you need to call
MockContextFactory.setDelegateEnvironment()
method. If your test runs inside the application server, you can simply
pass the environment returned by the current context (provided by the
app server):
MockContextFactory.setDelegateEnvironment( (new
InitialContext()).getEnvironment() );
Of course, you should do it before you call MockContextFactory.setAsInitial()
since this
call changes the JNDI environment.
Outside of the container, at the very minimum you need
to pass the initial context factory and the provider URL of the remote
context.
Top
Running EJB tests locally using MockEJB is not a
substitute for in-container
testing with your application server. Local tests are extremely
convenient for ongoing development work, but you still have to run the
in-container tests on a regular basis.
MockEJB allows you to run the same test class inside and
outside of the container with
minimal modifications. However, you might also want to have a
completely different set of tests for the in-container testing. These
tests may deal more with running "end-to-end" integration scenarios
than with testing individual EJBs. The approach for in-container
testing depends mostly on the complexity of your project and, to some
extend, the organization of the development team.
You should also keep in mind that MockEJB does not
support EJB deployment descriptors. So, to test the deployable EJBs,
developers should run in-container tests. If the same group of
developers is responsible for developing and deploying the code,
running the same tests inside and outside of the container might be
desirable.
MockEJB integrates with the Cactus framework for
in-container testing.
MockEJB allows you to switch between Cactus and local MockEJB testing
modes at any time. This is accomplished by using OptionalCactusTestCase
class as a superclass for your test classes. By default the test class
runs locally exactly the same way it would if it extended TestCase
directly. If the mockejb.cactus.mode system property is
set to true , OptionalCactusTestCase
delegates to org.apache.cactus.ServletTestCase
to run the test class. ServletTestCase then runs the
tests on the application server (assuming the test class and all tested
EJBs are deployed to this server). Alternatively, a test class can
override OptionalCactusTestCase.isCactusMode()
method and use a different mechanism for determining which mode to use
when it runs.
There might be differences in the setup code for your
test class depending on where it runs. For example, if you run the test
class locally, you may need to setup the data source by directly
instantiating a JDBC driver-provided class and binding it
to JNDI. When the test runs inside the container, you may want to rely
on the data source
pre-configured in the application server.
org.mockejb.OptionalCactusTestCase provides
method isRunningOnServer()
which returns true when the test runs inside the
container under the Cactus framework. So the code that sets up the data
source for out of container testing
should be enclosed into
if (!isRunningOnServer()) block.
The alternative approach is to use the inheritance. In
this case, your test class should only have the code required to run it
on the server. Its subclass should have the additional
setup code needed for running it locally. Both classes should override OptionalCactusTestCase.isCactusMode()
and return true and false respectively.
Top
Mock EJBs (i.e., mock EJB implementation classes, not to
be confused with MockEJB framework) do not have the real business
logic.
Instead, their logic is provided by the developer of a test class. That
way you can isolate EJBs under test from the rest of the application.
Suppose you've implemented FooBean which
calls BarBean . You also developed
FooBeanTest class to unit test FooBean .
In this case, you don't want FooBean to call the real BarBean .
BarBean implementation might change which will
require you to update FooBeanTest . Also, BarBean
may call other EJBs or use some resources provided by the application
server, which will complicate the setup code of your test class and
make the test class logic more brittle. The classic example is the
dependency on the entity beans and the database.
MockEJB framework allows you to replace the BarBean
business logic with the mock implementation that behaves exactly the
way expected by the test class. Its methods could return hardcoded
data, provide "stubs" for database operations and test the passed
parameters. The implementation class of a mock EJB can be defined as
the nested class of the test class. See
FundamentalsTest example for more details. You can also use easymock
or mockobjects frameworks.
Note that to implement a mock EJB, you only need to
provide the "bean" (implementation) class. Home and business interfaces
are shared with the "non-mock" EJB.
Outside of the container, mock EJBs are deployed the
same way with other EJBs using
MockContainer.deploy() .
Inside the container, mock EJBs rely on the ability of MockContext
to access delegate JNDI context. More specifically, Mock EJBs are
deployed using MockContainer.deploy() which puts them
into MockContext .
Non-mock EJBs are deployed by the application server which binds them
to its own JNDI tree. JNDI context of the application server should be
set as the delegate
context. During lookup() call, MockContext
first searches its
local context, so mock EJBs are always found first, even if the EJB
with the same name was already deployed by the application server.
If you want to run the same test class inside and
outside of the container, enclose MockContainer.deploy()
for tested ("non-mock") EJBs in if (! isRunningOnServer() )
block.
In this case, only mock EJBs will be deployed by the MockContainer ,
whereas EJBs under test will be deployed by the application server.
Packaging of mock EJBs for in-container testing requires
understanding of the application server's classloader hierarchy.
The usual practice with Cactus is to package test classes with the
Cactus web application. This approach, however, does not work for the
mock EJB implementation class. Normally, Web application classloader is
the child of EJB classloader, so the classes packaged with the web
application are not visible to EJBs.
So, Mock EJB implementation class should either be packaged with the
tested (calling) EJB classes or in another jar which is part of the
classpath of the tested EJB jar.
Top
MockEJB allows you to specify a chain (stack) of custom
objects that handle (intercept) EJB calls.
Interceptors are extremely powerful and can be used for a variety of
tasks. For example, interceptor can execute "asserts" for input
parameters or
return result of a method called indirectly from the test (e.g., test
calls EJB A, EJB A calls EJB B).
In MockEJB interceptors implement org.mockejb.interceptor.Interceptor
interface. Internally, interceptors are controlled by the org.mockejb.interceptor.ObjectInvoker
class. ObjectInvoker calls the interceptors according to
their order in the java.util.List which is part of ObjectInvoker
constructor. Each interceptor must do a callback to ObjectInvoker
using ObjectInvoker.invoke() method.
ObjectInvoker class also provides the
mechanism to access javax.ejb.SessionContext
and javax.ejb.MessageDrivenContext of the invoked EJB.
You can specify the list of interceptors explicitly in
the code of your test class.
Please see testCustomInterceptor() method of
FundamentalsTest example for more details.
MockEJB comes bundled with two system interceptors: ExceptionHandler
and TransactionManager
MockEJB always implicitly sets ExceptionHandler at the
head of the interceptor list. ExceptionHandler is
responsible for exception handling
according to the EJB specification. TransactionManager
always follows the ExceptionHandler in the interceptor
list (see "Using Tranactions" section).
Custom interceptors can be added using MockEjbObject.addInterceptor()
method. MockEjbObject.getInterceptorList() returns the
linked list of interceptors which you can
manipulate. This includes full access to system interceptors.
MockEJB also provides InvocationRecorder
interceptor.
Once added to the interceptor list, it stores the information about
method calls to the intercepted object. Later on, this information can
be queried by the test class. The structure of the preserved
information is defined by the Invocation
class.
InvocationRecorder can be selectively added to
the interceptor list of the EJBs you want to
check. In many cases, InvocationRecorder provides a
convenient alternative to the
custom interceptors.
Top
MockEJB supports container-managed transactions using TransactionManager
interceptor. MockEJB always puts this interceptor on the call chain
after ExceptionHandler . By default, the transaction
policy is "Supports" in which case TransactionManager
does not do anything.
To set a different
transaction policy, call MockEjbObject.setTransactionPolicy()
method. You can dynamically specify transaction policy that you need
for different methods thus emulating EJB deployment descriptor
settings.
For the most advanced transaction support needs, you can
extend TransactionManager and override its intercept()
method. In this method you can selectively set the policies that you
need for different EJB methods.
TransactionManager relies on javax.transaction.UserTransaction
object. This object must be available in JNDI tree (under
"javax.transaction.UserTransaction" key) except for "Supports" and
"NotSupported" policies. Alternatively, you can set UserTransaction
by calling TransactionManager.setUserTransaction()
method.
One way to obtain javax.transaction.UserTransaction
is to use JTA service provided by your application server. To do that,
you need to call MockContextFactory.setDelegateEnvironment()
to set the JNDI context of your application server as the delegate
context (as described in "Configuring JNDI"
section).
Of course, in this case, all resources that you use within your
transactions (such as DataSource) should support JTA.
Another option is to use the mock objects approach. For
example, Mockrunner
framework comes with the mock javax.transaction.UserTransaction
implementation.
TransactionManager supports all five
transaction policies. However, support for "RequiredNew" and
"NotSupported" has some limitations. Currently TransactionManager
is not able to suspend transactions, so it simply tries to call UserTransaction.begin()
in case of these policies. Most JTA implementations will throw the
exception in this case (since they don't support nested transactions).
JMS Support
Top
MockEJB provides JMS implementation that allows you to
test what messages are sent and in what order.
There are two administrative tasks that must be
performed before conducting a test:
- create destinations (queues and/or topics) and bind
them in JNDI context
- create connection factories and bind them in JNDI
context
Destinations are created by instantiating org.mockejb.jms.MockQueue
for queues, and org.mockejb.jms.MockTopic for topics.
Connections are created by instantiating org.mockejb.jms.QueueConnectionFactoryImpl
for queue connections, and org.mockejb.jms.TopicConnectionFactoryImpl
for topic connections.
What messages are sent and in what order can be checked
by examining the corresponded queues and topics.
All JMS related classes reside in the package org.mockejb.jms .
MockEJB JMS implementation is compatible with JMS versions 1.02b and
1.1.
|