- •contents
- •preface
- •acknowledgments
- •about this book
- •Special features
- •Best practices
- •Design patterns in action
- •Software directory
- •Roadmap
- •Part 1: JUnit distilled
- •Part 2: Testing strategies
- •Part 3: Testing components
- •Code
- •References
- •Author online
- •about the authors
- •about the title
- •about the cover illustration
- •JUnit jumpstart
- •1.1 Proving it works
- •1.2 Starting from scratch
- •1.3 Understanding unit testing frameworks
- •1.4 Setting up JUnit
- •1.5 Testing with JUnit
- •1.6 Summary
- •2.1 Exploring core JUnit
- •2.2 Launching tests with test runners
- •2.2.1 Selecting a test runner
- •2.2.2 Defining your own test runner
- •2.3 Composing tests with TestSuite
- •2.3.1 Running the automatic suite
- •2.3.2 Rolling your own test suite
- •2.4 Collecting parameters with TestResult
- •2.5 Observing results with TestListener
- •2.6 Working with TestCase
- •2.6.1 Managing resources with a fixture
- •2.6.2 Creating unit test methods
- •2.7 Stepping through TestCalculator
- •2.7.1 Creating a TestSuite
- •2.7.2 Creating a TestResult
- •2.7.3 Executing the test methods
- •2.7.4 Reviewing the full JUnit life cycle
- •2.8 Summary
- •3.1 Introducing the controller component
- •3.1.1 Designing the interfaces
- •3.1.2 Implementing the base classes
- •3.2 Let’s test it!
- •3.2.1 Testing the DefaultController
- •3.2.2 Adding a handler
- •3.2.3 Processing a request
- •3.2.4 Improving testProcessRequest
- •3.3 Testing exception-handling
- •3.3.1 Simulating exceptional conditions
- •3.3.2 Testing for exceptions
- •3.4 Setting up a project for testing
- •3.5 Summary
- •4.1 The need for unit tests
- •4.1.1 Allowing greater test coverage
- •4.1.2 Enabling teamwork
- •4.1.3 Preventing regression and limiting debugging
- •4.1.4 Enabling refactoring
- •4.1.5 Improving implementation design
- •4.1.6 Serving as developer documentation
- •4.1.7 Having fun
- •4.2 Different kinds of tests
- •4.2.1 The four flavors of software tests
- •4.2.2 The three flavors of unit tests
- •4.3 Determining how good tests are
- •4.3.1 Measuring test coverage
- •4.3.2 Generating test coverage reports
- •4.3.3 Testing interactions
- •4.4 Test-Driven Development
- •4.4.1 Tweaking the cycle
- •4.5 Testing in the development cycle
- •4.6 Summary
- •5.1 A day in the life
- •5.2 Running tests from Ant
- •5.2.1 Ant, indispensable Ant
- •5.2.2 Ant targets, projects, properties, and tasks
- •5.2.3 The javac task
- •5.2.4 The JUnit task
- •5.2.5 Putting Ant to the task
- •5.2.6 Pretty printing with JUnitReport
- •5.2.7 Automatically finding the tests to run
- •5.3 Running tests from Maven
- •5.3.2 Configuring Maven for a project
- •5.3.3 Executing JUnit tests with Maven
- •5.3.4 Handling dependent jars with Maven
- •5.4 Running tests from Eclipse
- •5.4.1 Creating an Eclipse project
- •5.4.2 Running JUnit tests in Eclipse
- •5.5 Summary
- •6.1 Introducing stubs
- •6.2 Practicing on an HTTP connection sample
- •6.2.1 Choosing a stubbing solution
- •6.2.2 Using Jetty as an embedded server
- •6.3 Stubbing the web server’s resources
- •6.3.1 Setting up the first stub test
- •6.3.2 Testing for failure conditions
- •6.3.3 Reviewing the first stub test
- •6.4 Stubbing the connection
- •6.4.1 Producing a custom URL protocol handler
- •6.4.2 Creating a JDK HttpURLConnection stub
- •6.4.3 Running the test
- •6.5 Summary
- •7.1 Introducing mock objects
- •7.2 Mock tasting: a simple example
- •7.3 Using mock objects as a refactoring technique
- •7.3.1 Easy refactoring
- •7.3.2 Allowing more flexible code
- •7.4 Practicing on an HTTP connection sample
- •7.4.1 Defining the mock object
- •7.4.2 Testing a sample method
- •7.4.3 Try #1: easy method refactoring technique
- •7.4.4 Try #2: refactoring by using a class factory
- •7.5 Using mocks as Trojan horses
- •7.6 Deciding when to use mock objects
- •7.7 Summary
- •8.1 The problem with unit-testing components
- •8.2 Testing components using mock objects
- •8.2.1 Testing the servlet sample using EasyMock
- •8.2.2 Pros and cons of using mock objects to test components
- •8.3 What are integration unit tests?
- •8.4 Introducing Cactus
- •8.5 Testing components using Cactus
- •8.5.1 Running Cactus tests
- •8.5.2 Executing the tests using Cactus/Jetty integration
- •8.6 How Cactus works
- •8.6.2 Stepping through a test
- •8.7 Summary
- •9.1 Presenting the Administration application
- •9.2 Writing servlet tests with Cactus
- •9.2.1 Designing the first test
- •9.2.2 Using Maven to run Cactus tests
- •9.2.3 Finishing the Cactus servlet tests
- •9.3 Testing servlets with mock objects
- •9.3.1 Writing a test using DynaMocks and DynaBeans
- •9.3.2 Finishing the DynaMock tests
- •9.4 Writing filter tests with Cactus
- •9.4.1 Testing the filter with a SELECT query
- •9.4.2 Testing the filter for other query types
- •9.4.3 Running the Cactus filter tests with Maven
- •9.5 When to use Cactus, and when to use mock objects
- •9.6 Summary
- •10.1 Revisiting the Administration application
- •10.2 What is JSP unit testing?
- •10.3 Unit-testing a JSP in isolation with Cactus
- •10.3.1 Executing a JSP with SQL results data
- •10.3.2 Writing the Cactus test
- •10.3.3 Executing Cactus JSP tests with Maven
- •10.4 Unit-testing taglibs with Cactus
- •10.4.1 Defining a custom tag
- •10.4.2 Testing the custom tag
- •10.5 Unit-testing taglibs with mock objects
- •10.5.1 Introducing MockMaker and installing its Eclipse plugin
- •10.5.2 Using MockMaker to generate mocks from classes
- •10.6 When to use mock objects and when to use Cactus
- •10.7 Summary
- •Unit-testing database applications
- •11.1 Introduction to unit-testing databases
- •11.2 Testing business logic in isolation from the database
- •11.2.1 Implementing a database access layer interface
- •11.2.2 Setting up a mock database interface layer
- •11.2.3 Mocking the database interface layer
- •11.3 Testing persistence code in isolation from the database
- •11.3.1 Testing the execute method
- •11.3.2 Using expectations to verify state
- •11.4 Writing database integration unit tests
- •11.4.1 Filling the requirements for database integration tests
- •11.4.2 Presetting database data
- •11.5 Running the Cactus test using Ant
- •11.5.1 Reviewing the project structure
- •11.5.2 Introducing the Cactus/Ant integration module
- •11.5.3 Creating the Ant build file step by step
- •11.5.4 Executing the Cactus tests
- •11.6 Tuning for build performance
- •11.6.2 Grouping tests in functional test suites
- •11.7.1 Choosing an approach
- •11.7.2 Applying continuous integration
- •11.8 Summary
- •Unit-testing EJBs
- •12.1 Defining a sample EJB application
- •12.2 Using a façade strategy
- •12.3 Unit-testing JNDI code using mock objects
- •12.4 Unit-testing session beans
- •12.4.1 Using the factory method strategy
- •12.4.2 Using the factory class strategy
- •12.4.3 Using the mock JNDI implementation strategy
- •12.5 Using mock objects to test message-driven beans
- •12.6 Using mock objects to test entity beans
- •12.7 Choosing the right mock-objects strategy
- •12.8 Using integration unit tests
- •12.9 Using JUnit and remote calls
- •12.9.1 Requirements for using JUnit directly
- •12.9.2 Packaging the Petstore application in an ear file
- •12.9.3 Performing automatic deployment and execution of tests
- •12.9.4 Writing a remote JUnit test for PetstoreEJB
- •12.9.5 Fixing JNDI names
- •12.9.6 Running the tests
- •12.10 Using Cactus
- •12.10.1 Writing an EJB unit test with Cactus
- •12.10.2 Project directory structure
- •12.10.3 Packaging the Cactus tests
- •12.10.4 Executing the Cactus tests
- •12.11 Summary
- •A.1 Getting the source code
- •A.2 Source code overview
- •A.3 External libraries
- •A.4 Jar versions
- •A.5 Directory structure conventions
- •B.1 Installing Eclipse
- •B.2 Setting up Eclipse projects from the sources
- •B.3 Running JUnit tests from Eclipse
- •B.4 Running Ant scripts from Eclipse
- •B.5 Running Cactus tests from Eclipse
- •references
- •index
Testing components using mock objects |
167 |
|
|
In order to be able to test this method, you need to get hold of a valid HttpServletRequest object. Unfortunately, it is not possible to call new HttpServletRequest to create a usable request. The life cycle of HttpServletRequest is managed by the container. JUnit alone is not enough to write a test for the isAuthenticated method.
DEFINITION component/container—A component is a piece of code that executes inside a container. A container is a receptacle that offers useful services for the components it is hosting, such as life cycle, security, transaction, distribution, and so forth.
Fortunately, several solutions are available; we’ll cover them in the following sections. Overall, we’re presenting two core solutions: outside-the-container testing with mock objects and in-container testing using Cactus.
A variation on these two approaches would be to use a stubbed container, such as the ServletUnit module in HttpUnit (http://httpunit.sourceforge.net/). Unfortunately, we haven’t found a full-blown implementation of a stub J2EE container. ServletUnit implements only a portion of the Servlet specification. (For more about stubs, see chapter 6.)
Mock objects and Cactus are both valid approaches. In chapter 7, we discussed the advantages and drawbacks of the mock objects outside-the-container strategy. In this chapter, we focus on the Cactus in-container strategy to show its relative advantages and drawbacks. Chapter 9 turns the focus to unit-testing servlets. In that chapter, we discuss when to use mock objects and when to use Cactus tests when you’re testing servlets.
8.2 Testing components using mock objects
Let’s try using mock objects to test a servlet and then discuss the benefits and drawbacks of this approach. In chapter 7, you built your own mock objects. Fortunately, several frameworks are available that automate the generation of mock objects. In this chapter you’ll use EasyMock (http://www.easymock.org/— version 1.0 or above).
EasyMock is one of the best-known mock-object generators today. We’ll demonstrate how to use some other generators in later chapters. EasyMock is implemented using Java dynamic proxies so that it can transparently generate mock objects at runtime, as shown in the next section.
168CHAPTER 8
In-container testing with Cactus
8.2.1Testing the servlet sample using EasyMock
The solution that comes to mind to unit-test the isAuthenticated method (listing 8.1) is to mock the HttpServletRequest class using the mock objects approach described in chapter 7. This approach would work, but you would need to write quite a few lines of code. The result can be easily achieved using the EasyMock framework as demonstrated in listing 8.2.
Listing 8.2 Using EasyMock to unit-test servlet code
package junitbook.container;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession;
import org.easymock.MockControl; import junit.framework.TestCase;
public class TestSampleServlet extends TestCase
{
private SampleServlet servlet; |
|
|
|
private MockControl controlHttpServlet; |
|
b |
|
|
|||
private HttpServletRequest mockHttpServletRequest; |
|
||
|
|
|
|
private MockControl controlHttpSession; |
|
|
|
private HttpSession mockHttpSession; |
|
|
|
protected void setUp() |
|
|
|
{ |
|
|
|
servlet = new SampleServlet(); |
|
|
|
controlHttpServlet = MockControl.createControl( |
|
|
b |
|
|
||
HttpServletRequest.class); |
|
|
|
mockHttpServletRequest = |
|
|
|
(HttpServletRequest) controlHttpServlet.getMock(); |
|
|
|
controlHttpSession = MockControl.createControl( |
|
|
|
HttpSession.class); |
|
|
|
mockHttpSession = |
|
|
|
(HttpSession) controlHttpSession.getMock(); |
|
|
|
} |
|
|
|
protected void tearDown()
{
controlHttpServlet.verify();
controlHttpSession.verify();
}
c
public void testIsAuthenticatedAuthenticated()
{
Testing components using mock objects |
169 |
|
mockHttpServletRequest.getSession(false); |
|
d |
|
||
|
||
controlHttpServlet.setReturnValue(mockHttpSession); |
|
|
mockHttpSession.getAttribute("authenticated"); |
|
e |
|
||
controlHttpSession.setReturnValue("true"); |
|
|
controlHttpServlet.replay(); |
|
f |
|
||
controlHttpSession.replay(); |
|
|
assertTrue(servlet.isAuthenticated(mockHttpServletRequest)); |
|
|
}
public void testIsAuthenticatedNotAuthenticated()
{
mockHttpServletRequest.getSession(false); controlHttpServlet.setReturnValue(mockHttpSession);
mockHttpSession.getAttribute("authenticated"); controlHttpSession.setReturnValue(null);
controlHttpServlet.replay();
controlHttpSession.replay();
assertFalse(
servlet.isAuthenticated(mockHttpServletRequest));
}
public void testIsAuthenticatedNoSession()
{
mockHttpServletRequest.getSession(false); controlHttpServlet.setReturnValue(null); controlHttpServlet.replay(); controlHttpSession.replay();
assertFalse(
servlet.isAuthenticated(mockHttpServletRequest));
}
}
d
g
f
h f
bCreate two mocks, one of HttpServletRequest and one of HttpSession. EasyMock works by creating a dynamic proxy for the mock, which is controlled through a
control object (MockControl).
cTell EasyMock to verify that the calls to the mocked methods have effectively happened. EasyMock reports a failure if you have defined a mocked method that is not
called in the test. It also reports a failure if a method for which you have not defined any behavior is called. You do this in JUnit’s tearDown method so that the verification is done automatically for all tests. For this factorization to work for all the tests, you have to activate the HttpSession mock in testIsAuthenticatedNoSession, even
170CHAPTER 8
In-container testing with Cactus
though you don’t use it in that test. With EasyMock, a mock can only be verified if it has been activated.
dUsing the control object, tell the HttpServletRequest mock that when getSession(false) is called, the method should return the HttpSession mock.
e g h Do the same thing as in the previous step, telling the mock how to behave when such-and-such method is called.
d e Notice that in order to tell the mocks how to behave, you call their methods and g h then tell the control object what the method should return. This is the EasyMock
training mode.
fOnce you activate the mocks here, you step out of the training mode, and calls to the mock methods return the expected results.
The result of executing this TestSampleServlet TestCase in Eclipse is shown in figure 8.1. (Of course, other IDEs would yield a similar result.)
8.2.2Pros and cons of using mock objects to test components
The biggest advantage of the mock-object approach is that it does not require a running container in order to execute the tests. The tests can be set up quickly, and they run fast. The main drawback is that the components are not tested in the container in which they will be deployed. You don’t get to test the interactions with the container, which is an important part of the code. You’re also not testing the interactions between the different components when they are run inside the container.
You still need a way to perform integration testing. Writing and running functional tests could achieve this. The problem with functional tests is that they are coarse-grained and test only a full use case—you lose the benefits of fine-grained
Figure 8.1
Result of executing the mock object TestCase for testing isAuthenticated
Testing components using mock objects |
171 |
|
|
unit testing. You also know that you won’t be able to test as many different cases with functional tests as you can with unit tests.
There are also other possible disadvantages of using the mock-objects approach. For example, you may have a lot of mock objects to set up; this may prove to be a non-negligible overhead. Obviously, the cleaner the code is (especially with small and focused methods), the easier tests are to set up.
Another important drawback with the mock-objects approach is that in order to set up the test, you usually must know exactly how the API being mocked behaves. It’s easy to know that behavior for your own API, but not so easy for an external API, like the Servlet API.
Even though they all implement the same API, all containers do not behave in the same way. For example, take the following piece of code:
public void doGet(HttpServletRequest request,
HttpServletResponse response)
{
response.setContentType("text/xml");
}
Seems simple enough, but, if you run it in Tomcat 4.x (http://jakarta.apache.org/ tomcat/) and in Orion 1.6.0 (http://www.orionserver.com/), you’ll notice different behaviors. In Tomcat, the returned content type is text/xml, but in Orion it’s text/html.
This may seem like an extreme example, and it is. All servlet containers manage to implement the API in pretty much the same way. But the situation is far worse for the J2EE APIs—especially the EJB APIs. A specification is a document, and sometimes it’s possible to interpret a document in different ways. A specification can even be inconsistent, making it impossible to implement it to the letter. (Try talking to servlet container implementers about the Servlet Filters API!) And, of course, containers have bugs. (The example we have shown may be a bug in Orion 1.6.0.) It’s an unfortunate fact of life that you have to live with bugs when you’re writing an application.
To summarize, the drawbacks of using mock objects for unit-testing J2EE components are that they:
■Do not test interactions with the container or between the components
■Do not test the deployment part of the components
■Need excellent knowledge of the API being called in order to mock it, which can be difficult (especially for external libraries)
■Do not provide confidence that the code will run in the target container