2011년 6월 9일 목요일

Container Driven Testing: Advanced EJB Testing with OpenEJB-2

June 2004


"Behold your new King, same as the old King"—Dio

Introduction

This section covers techniques and strategies for testing your Entity Beans. Entity Beans come with their own special considerations and issues, because in addition to the bean classes and the test cases, you must also concern yourself with generating a database.

Part of what makes unit testing so attractive as a development process is that you can automate your tests in your project's build environment. Testing EJBs also requires automating the generation and destruction of a database at runtime, and can present some challenges. I've assembled some of the trickier build-related pitfalls and techniques that you'll want to use when setting up your Supreme Killer Application Testing Environment (SKATE).

Let's start out by running some tests against our entity beans.

Running the Example Tests

1. Download the code for part 2 from http://www.openejb.org/ejb-testing-examples-part02.zip

2. Unzip to C:\

3. Check your installation:

C:\>dir ejb-testing-examples-part02

Directory of C:\ejb-testing-examples-part02

05/16/2004  08:09p      <DIR>          .
05/16/2004  08:09p      <DIR>          ..
05/16/2004  07:53p      <DIR>          example_01
05/16/2004  07:53p      <DIR>          example_02
              0 File(s)              0 bytes
      3 Dir(s)  15,507,103,744 bytes free

4. Change directory to C:\ejb-testing-examples-part02\example_02:

C:\> cd ejb-testing-examples-part02\example_02

5. Generate the Database:

C:\ebj-testing-examples-part02\example_02> maven db:setup
__  __
|  \/  |__ _Apache__ ___
| |\/| / _` \ V / -_) ' \  ~ intelligent projects ~
|_|  |_\__,_|\_/\___|_||_|  v. 1.0-rc2

Attempting to download openejb-core-1.0-SNAPSHOT.jar.
build:start:

db:setup:
   [sql] Executing file: C:\ejb-testing-examples\example_02\src\sql\setupEmployee.sql
   [sql] 10 of 10 SQL statements executed successfully
BUILD SUCCESSFUL
Total time: 6 seconds

6. Compile your EJB JAR and execute the entity tests:

C:\ejb-testing-examples-part02\example_02>maven
__  __
|  \/  |__ _Apache__ ___
| |\/| / _` \ V / -_) ' \  ~ intelligent projects ~
|_|  |_\__,_|\_/\___|_||_|  v. 1.0-rc2

Attempting to download openejb-core-1.0-SNAPSHOT.jar.
build:start:

default:
   [mkdir] Created dir: C:\ejb-testing-examples-part02\example_02\logs
java:prepare-filesystem:
   [mkdir] Created dir: C:\ejb-testing-examples-part02\example_02\target\classes

java:compile:
   [echo] Compiling to C:\ejb-testing-examples-part02\example_02/target/classes
   [javac] Compiling 7 source files to C:\ejb-testing-examples-part02\example_02\target\c
lasses

java:jar-resources:
Copying 1 file to C:\ejb-testing-examples-part02\example_02\target\classes

test:prepare-filesystem:
   [mkdir] Created dir: C:\ejb-testing-examples-part02\example_02\target\test-classes
   [mkdir] Created dir: C:\ejb-testing-examples-part02\example_02\target\test-reports

test:test-resources:

test:compile:

test:test:

jar:jar:
   [jar] Building jar: C:\ejb-testing-examples-part02\example_02\target\ejb-testing-examp
les-1.0.jar
aspectj:init:


jar:

validate:

java:prepare-filesystem:

java:compile:
   [echo] Compiling to C:\ejb-testing-examples-part02\example_02/target/classes

java:jar-resources:

test:prepare-filesystem:

test:test-resources:

test:compile:
   [javac] Compiling 5 source files to C:\ejb-testing-examples-part02\example_02\target\test-classes
Note: Some input files use or override a deprecated API.
Note: Recompile with -deprecation for details.

test:test:
   [junit] Running com.nrfx.articles.openejb.EmployeeAccessorsTest
   [junit] Tests run: 4, Failures: 0, Errors: 0, Time elapsed: 4.917 sec
   [junit] Running com.nrfx.articles.openejb.EmployeeCreateTest
   [junit] Tests run: 2, Failures: 0, Errors: 0, Time elapsed: 4.347 sec
   [junit] Running com.nrfx.articles.openejb.EmployeeFindTest
   [junit] Tests run: 5, Failures: 0, Errors: 0, Time elapsed: 7.641 sec
BUILD SUCCESSFUL
Total time: 31 seconds

Congratulations!

You just executed a group of tests on an Entity Bean component, directly against an embedded EJB Container. The first thing you might notice is that the Entity Bean tests took a bit longer than the Session Bean tests did in Part One. That's because they've got to work with the database (our example uses Axion), and also because each TestCase loads in its own VM in our Maven build, meaning the context to the database and the container need to be rebuilt for each test case. There is a way to address this and speed up your entity bean tests, and I'll show you the secret later on. First, though, let's take a look inside the EmployeeAccessorsTest.java file, so we can see how these tests are put together.

Inside the Test Class

Inside the EmployeeAccessorsTest.java file, you'll see we employ the exact same strategy for getting an InitialContext to the embedded EJB Container, and that we also get our Entity Bean object in exactly the same way we would a Session Bean:

package com.nrfx.articles.openejb;

import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import junit.framework.TestCase;

/**
* JUnit test cases for the get/set methods
* of the Employee EJB
*
* @see Employee
* @see EmployeeHome
*/
public class EmployeeAccessorsTest
       extends TestCase {

   private Employee employee;

   protected void setUp() throws Exception {
       InitialContext context =
           new InitialContext(System.
               getProperties());

       Object obj = context.lookup("Employee");

       EmployeeHome employeeHome =
           (EmployeeHome)PortableRemoteObject.
               narrow( obj, EmployeeHome.class);

       employee = employeeHome.
           findByPrimaryKey(new Integer(1));
   }

   public void testFirstName() {
       String expected = "Miles";
       String actual   = null;
       try{
           employee.setFirstName(expected);
       } catch (Exception e){
           fail("Can't set FirstName: " +
               "Received Exception: " +
               e.getClass() + " : " +
               e.getMessage());
       }

       try{
           actual = employee.getFirstName();
           assertEquals("The FirstName of " +
               "the Employee is not the same.",
               expected, actual);

       } catch (Exception e) {
           fail("Can't get FirstName: " +
               "Received Exception " +
               e.getClass() + " : " +
               e.getMessage());
       }
   }
   // ... Add as many test methods as you like
}

This test case in every way resembles the Session Bean test case we looked at in the first part of this series. It relies solely on the standard EJB API, JNDI, and the system properties. It incorporates no OpenEJB-specific classes, and operates as a standard unit test. After getting an InitialContext to the embedded EJB Container, you can use the finder methods in the Entity Bean's Home interface to get an instance of the bean from the Container. Once you have your Entity Bean, you can fire off as many tests as you like against its accessor and mutator methods.

Consider what this means for the infamous EJB learning curve. With the ability to write tests in this manner for both Entity and Session Beans, the process of learning and mastering EJBs suddenly becomes much more transparent and straightforward. Container driven testing demystifies and accelerates the process of developing EJBs in a way which no faux environment ever could.

Secrets of Entity Bean Testing

Entity Beans have a whole basket of special considerations which must be factored into your testing strategy. Unit testing Entity Beans is not a well-established art, and there are some techniques and trivia that will save you time and frustration as you get started. For the remainder of this section we're going to cover the following three topics:

  • Testing Against a Database
  • Mandatory Tests for Entity Beans (CRUD Operations)
  • How to Test Transaction Integrity & Rollback

Testing Against a Database

One of the biggest challenges you're going to face when you start testing your Entity Bean components is how to automatically keep the contents of your database in a consistent state. Fortunately, we've scouted out the terrain for you and we know where the bodies are buried. While there are still no perfect solutions to this challenge, we can at least get you started.

It seems tempting to assemble your data set in the setUp method of your test class and clean it up in the tearDown method, but this strategy will seriously impact performance and should be avoided if possible. Also, in most cases, when one of your test methods fails, the build tool immediately fails out. This could leave you with an altered data set, which you must then tear down and rebuild by hand.

The process of building and maintaining a reliable data set would be a bit easier if JUnit guaranteed us a setUp and tearDown for every test suite, every test case, and every test method. It's not difficult to set up the data for an Entity Bean test case in its constructor, but there's no deconstructor method that always gets called when all the test methods are finished. It is possible to add this functionality to the OpenEJB testing framework, but then you'd be tied to their API, which wouldn't do at all.

This level of fine-grained control over every part of a running test suite was never really necessary before, but with the capability to test an entire system of interacting enterprise components, its usefulness becomes pretty obvious.

And so, we relegate the setting up and tearing down of the database to the build environment, a strategy that has its own drawbacks. We use an Open Source database called Axion in our example tests. One of the difficulties of using Axion is that it cannot handle multiple concurrent threads at runtime. This makes it difficult for Maven to automatically and reliably set up and tear down the test database at build-time.

The first issue is caused by the <ant:sql> tag, which forks the VM at runtime. Because the Axion database doesn't let more that one VM access it at one time, running a concurrent test goal (which also forks off a new VM) will not work. Using an industry-strength database like MySQL or PostgreSQL would actually make it easier to use Maven's preGoal and postGoal features to automatically run the db:setup and db:teardown before and after you run the tests.

However, even if you manage to get around the single VM restriction, test failures in JUnit kill the Maven process before a <postGoal> can be executed, meaning there is no way to guarantee that the data is being properly torn down. There has been some discussion of revamping the <postGoal> behavior inside Maven to ensure that it gets run, regardless of whether or not the thread is failing.

For the mean time, know that these issues are well-documented and are being addressed. I ask that you do as we do when setting up and tearing down your data:

$ maven db:setup
...
$ maven
...
$ maven db:teardown
...

Mandatory Tests for Entity Beans (CRUD Operations)

The most important functionality that you should test in your entity beans is that they reliably and predictably can perform the basic Create, Read, Update, and Delete operations. You need to know that they're being build correctly against the database, and although you may not like it, this might require you to write some raw JDBC code.

You might balk at the thought of having to write JDBC code in your tests, but time and practice have shown us that unit testing can actually speed up development, and there's no reason we should ignore the most crucial aspects of an Entity Bean's functionality. Who knows how you expect the data to actually look in the database, or how it might differ once it's been encapsulated into an object structure. The best way to explicitly define this behavior is to test for it. Besides, there's a lot more to EJB than simplified persistence, and once you master these aspects of your Entity Beans, you'll be able to move on to more advanced functionality.

Writing example code for illustration how to test CRUD operations is beyond the scope of this article, but here's some pseudocode to get you started:

Entity read accuracy:
1.  Directly insert a record with JDBC.
2.  findByPrimaryKey
3.  assert: all data should match

Entity finder accuracy:
1.  Directly insert N records with column Foo set to 'bar'
2.  findByFoo("bar")
3.  assert: number of entities return should be N

Entity update accuracy:
1.  Directly insert a record with JDBC
2.  findByPrimaryKey to get the Entity version
3.  call set methods on entity
4.  Directly select the record with JDBC
5.  assert: all data should match

Entity create accuracy:
1.  ejbObject = home.create(...)
2.  ejbObject.getPrimaryKey()
3.  Directly select the record with JDBC
4.  assert: all data should match

Entity delete accuracy 1:
1.  Directly insert a record with JDBC.
2.  home.remove(primaryKey)
3.  Directly select the record with JDBC
4.  assert: record should not exist

Entity delete accuracy 2:
1.  Directly insert a record with JDBC.
2.  ejbObject = home.findByPrimaryKey(..)
3.  ejbObject.remove()
4.  Directly select the record with JDBC,
5.  assert: record should not exist.

Testing Transaction Integrity & Rollback

When you're testing CRUD operations during transactions, you need to remember that there are not one but two types of successful transactions. The ones that successfully alter the contents of the database, and the ones which successfully do not alter the contents of the database. A successful transaction might be a failed transaction--it's better for the transaction to roll back than to muddy up the database with a partial write.

As with the thread-safety tests and CRUD operation accuracy tests, there's just not enough room in this article to show example code for every type of entity bean test which should be written. I can give you some pseudocode to get you started, though.

Entity update accuracy GOOD TX:
1.  Directly insert a record with JDBC
2.  findByPrimaryKey to get the Entity version
3.  userTransaction.begin()
4.  call set methods on entity
5.  userTransaction.commit()
6.  Directly select the record with JDBC and compare the data.
7.  assert: The data should match.

Entity update accuracy BAD TX:
1.  Directly insert a record with JDBC
2.  findByPrimaryKey to get the Entity version
3.  userTransaction.begin()
4.  call set methods on entity
5.  userTransaction.rollback()
6.  Directly select the record with JDBC and compare the data.
7.  assert: The data should NOT match modified state.
8.  assert: The data SHOULD match original state.

Entity create accuracy GOOD TX:
1.  userTransaction.begin()
2.  ejbObject = home.create(...)
3.  ejbObject.getPrimaryKey()
4.  userTransaction.commit()
5.  Directly select the record with JDBC
6.  assert: record should exist
7.  assert: data should match

Entity create accuracy BAD TX:
1.  userTransaction.begin()
2.  ejbObject = home.create(...)
3.  ejbObject.getPrimaryKey()
4.  userTransaction.rollback()
5.  Directly select the record with JDBC
6.  assert: record should NOT exist

Entity delete accuracy 1 GOOD TX:
1.  Directly insert a record with JDBC.
2.  userTransaction.begin()
3.  home.remove( primaryKey )
4.  userTransaction.commit()
5.  Directly select the record with JDBC
6.  assert: should not exist

Entity delete accuracy 1 BAD TX:
1.  Directly insert a record with JDBC.
2.  userTransaction.begin()
3.  home.remove( primaryKey )
4.  userTransaction.rollback()
5.  Directly select the record with JDBC
6.  assert: should still exist

Entity delete accuracy 2 GOOD TX:
1.  Directly insert a record with JDBC.
2.  userTransaction.begin()
3.  ejbObject = home.findByPrimaryKey(..)
4.  ejbObject.remove()
5.  userTransaction.commit()
6.  Directly select the record with JDBC,
7.  assert: record should not exist.

Entity delete accuracy 2 BAD TX:
1.  Directly insert a record with JDBC.
2.  userTransaction.begin()
3.  ejbObject = home.findByPrimaryKey(..)
4.  ejbObject.remove()
5.  userTransaction.rollback()
6.  Directly select the record with JDBC,
7.  assert: record should still exist.

None of the above functionality can actually be tested in a faux-container environment. It just can't be done—not with precision or accuracy, and certainly not without littering your code with non-standard APIs. The only way to test these sorts of behaviors is inside the container.

Coming Up

Part three of this series will cover more advanced techniques and the role of Mock Objects in EJB testing. We'll also talk about OpenEJB's biggest weakness, and what the future will hold for OpenEJB.

About the Author

N. Alex Rupp is a professional Open Source developer and software architect for Open Technology Systems, a Twin Cities firm dedicated to helping small and medium sized companies participate in the Open Source software movement. He frequently writes code at the Dunn Brothers coffee shop in southeast Minneapolis. He is also President of the Twin Cities chapter of IASA, the International Association of Software Architects, and JSR-94 implementation lead for Drools, a dynamic rule engine project at The Codehaus.

He can be reached at alex@nrfx.com

댓글 없음:

댓글 쓰기

ETL 솔루션 환경

ETL 솔루션 환경 하둡은 대용량 데이터를 값싸고 빠르게 분석할 수 있는 길을 만들어줬다. 통계분석 엔진인 “R”역시 하둡 못지 않게 관심을 받고 있다. 빅데이터 역시 데이터라는 점을 볼때 분산처리와 분석 그 이전에 데이터 품질 등 데이...