Files
spring-batch/build/reference/html/testing.html
Michael Minella 75ab909314 update
2017-03-23 10:18:33 -05:00

194 lines
18 KiB
HTML

<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>10.&nbsp;Unit Testing</title><link rel="stylesheet" type="text/css" href="css/manual-multipage.css"><meta name="generator" content="DocBook XSL Stylesheets V1.78.1"><link rel="home" href="index.html" title="Spring Batch - Reference Documentation"><link rel="up" href="index.html" title="Spring Batch - Reference Documentation"><link rel="prev" href="retry.html" title="9.&nbsp;Retry"><link rel="next" href="patterns.html" title="11.&nbsp;Common Batch Patterns"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><table width="100%" summary="Navigation header"><tr><th colspan="3" align="center">10.&nbsp;Unit Testing</th></tr><tr><td width="20%" align="left"><a accesskey="p" href="retry.html">Prev</a>&nbsp;</td><th width="60%" align="center">&nbsp;</th><td width="20%" align="right">&nbsp;<a accesskey="n" href="patterns.html">Next</a></td></tr></table><hr></div><div class="chapter"><div class="titlepage"><div><div><h1 class="title"><a name="testing" href="#testing"></a>10.&nbsp;Unit Testing</h1></div></div></div><p>Just as with other application styles, it is extremely important to
unit test any code written as part of a batch job as well. The Spring core
documentation covers how to unit and integration test with Spring in great
detail, so it won't be repeated here. It is important, however, to think
about how to 'end to end' test a batch job, which is what this chapter will
focus on. The spring-batch-test project includes classes that will help
facilitate this end-to-end test approach.</p><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="creatingUnitTestClass" href="#creatingUnitTestClass"></a>10.1&nbsp;Creating a Unit Test Class</h2></div></div></div><p>In order for the unit test to run a batch job, the framework must
load the job's ApplicationContext. Two annotations are used to trigger
this:</p><div class="itemizedlist"><ul class="itemizedlist" style="list-style-type: disc; "><li class="listitem"><p><code class="classname">@RunWith(SpringJUnit4ClassRunner.class)</code>:
Indicates that the class should use Spring's JUnit facilities</p></li><li class="listitem"><p><code class="classname">@ContextConfiguration(locations = {...})</code>:
Indicates which XML files contain the ApplicationContext.</p></li></ul></div><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@RunWith(SpringJUnit4ClassRunner.class)</span></em>
<em><span class="hl-annotation" style="color: gray">@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",
"/jobs/skipSampleJob.xml" })</span></em>
<span class="hl-keyword">public</span> <span class="hl-keyword">class</span> SkipSampleFunctionalTests { ... }</pre></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="endToEndTesting" href="#endToEndTesting"></a>10.2&nbsp;End-To-End Testing of Batch Jobs</h2></div></div></div><p>'End To End' testing can be defined as testing the complete run of a
batch job from beginning to end. This allows for a test that sets up a
test condition, executes the job, and verifies the end result.</p><p>In the example below, the batch job reads from the database and
writes to a flat file. The test method begins by setting up the database
with test data. It clears the CUSTOMER table and then inserts 10 new
records. The test then launches the <code class="classname">Job </code>using the
<code class="methodname">launchJob()</code> method. The
<code class="methodname">launchJob</code>() method is provided by the
<code class="classname">JobLauncherTestUtils</code> class. Also provided by the
utils class is <code class="classname">launchJob(JobParameters)</code>, which
allows the test to give particular parameters. The
<code class="methodname">launchJob()</code> method returns the
<code class="classname">JobExecution</code> object which is useful for asserting
particular information about the <code class="classname">Job</code> run. In the
case below, the test verifies that the <code class="classname">Job</code> ended
with status "COMPLETED".</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@RunWith(SpringJUnit4ClassRunner.class)</span></em>
<em><span class="hl-annotation" style="color: gray">@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",
"/jobs/skipSampleJob.xml" })</span></em>
<span class="hl-keyword">public</span> <span class="hl-keyword">class</span> SkipSampleFunctionalTests {
<em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
<span class="hl-keyword">private</span> JobLauncherTestUtils jobLauncherTestUtils;
<span class="hl-keyword">private</span> SimpleJdbcTemplate simpleJdbcTemplate;
<em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
<span class="hl-keyword">public</span> <span class="hl-keyword">void</span> setDataSource(DataSource dataSource) {
<span class="hl-keyword">this</span>.simpleJdbcTemplate = <span class="hl-keyword">new</span> SimpleJdbcTemplate(dataSource);
}
<em><span class="hl-annotation" style="color: gray">@Test</span></em>
<span class="hl-keyword">public</span> <span class="hl-keyword">void</span> testJob() <span class="hl-keyword">throws</span> Exception {
simpleJdbcTemplate.update(<span class="hl-string">"delete from CUSTOMER"</span>);
<span class="hl-keyword">for</span> (<span class="hl-keyword">int</span> i = <span class="hl-number">1</span>; i &lt;= <span class="hl-number">10</span>; i++) {
simpleJdbcTemplate.update(<span class="hl-string">"insert into CUSTOMER values (?, 0, ?, 100000)"</span>,
i, <span class="hl-string">"customer"</span> + i);
}
JobExecution jobExecution = jobLauncherTestUtils.launchJob().getStatus();
Assert.assertEquals(<span class="hl-string">"COMPLETED"</span>, jobExecution.getExitStatus());
}
}</pre></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="testingIndividualSteps" href="#testingIndividualSteps"></a>10.3&nbsp;Testing Individual Steps</h2></div></div></div><p>For complex batch jobs, test cases in the end-to-end testing
approach may become unmanageable. It these cases, it may be more useful to
have test cases to test individual steps on their own. The
<code class="classname">AbstractJobTests</code> class contains a method
<code class="methodname">launchStep</code> that takes a step name and runs just
that particular <code class="classname">Step</code>. This approach allows for more
targeted tests by allowing the test to set up data for just that step and
to validate its results directly.</p><pre class="programlisting">JobExecution jobExecution = jobLauncherTestUtils.launchStep(<span class="hl-string">"loadFileStep"</span>);</pre></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="d5e3514" href="#d5e3514"></a>10.4&nbsp;Testing Step-Scoped Components</h2></div></div></div><p>Often the components that are configured for your steps at runtime
use step scope and late binding to inject context from the step or job
execution. These are tricky to test as standalone components unless you
have a way to set the context as if they were in a step execution. That is
the goal of two components in Spring Batch: the
<code class="classname">StepScopeTestExecutionListener</code> and the
<code class="classname">StepScopeTestUtils</code>.</p><p>The listener is declared at the class level, and its job is to
create a step execution context for each test method. For example:</p><pre class="programlisting"><em><span class="hl-annotation" style="color: gray">@ContextConfiguration</span></em>
<em><span class="hl-annotation" style="color: gray">@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,
StepScopeTestExecutionListener.class })</span></em>
<em><span class="hl-annotation" style="color: gray">@RunWith(SpringJUnit4ClassRunner.class)</span></em>
<span class="hl-keyword">public</span> <span class="hl-keyword">class</span> StepScopeTestExecutionListenerIntegrationTests {
<span class="hl-comment">// This component is defined step-scoped, so it cannot be injected unless</span>
<span class="hl-comment">// a step is active...</span>
<em><span class="hl-annotation" style="color: gray">@Autowired</span></em>
<span class="hl-keyword">private</span> ItemReader&lt;String&gt; reader;
<span class="hl-keyword">public</span> StepExecution getStepExection() {
StepExecution execution = MetaDataInstanceFactory.createStepExecution();
execution.getExecutionContext().putString(<span class="hl-string">"input.data"</span>, <span class="hl-string">"foo,bar,spam"</span>);
<span class="hl-keyword">return</span> execution;
}
<em><span class="hl-annotation" style="color: gray">@Test</span></em>
<span class="hl-keyword">public</span> <span class="hl-keyword">void</span> testReader() {
<span class="hl-comment">// The reader is initialized and bound to the input data</span>
assertNotNull(reader.read());
}
}</pre><p>There are two <code class="classname">TestExecutionListeners</code>, one
from the regular Spring Test framework and handles dependency injection
from the configured application context, injecting the reader, and the
other is the Spring Batch
<code class="classname">StepScopeTestExecutionListener</code>. It works by looking
for a factory method in the test case for a
<code class="classname">StepExecution</code>, and using that as the context for
the test method, as if that execution was active in a Step at runtime. The
factory method is detected by its signature (it just has to return a
<code class="classname">StepExecution</code>). If a factory method is not provided
then a default <code class="classname">StepExecution</code> is created.</p><p>The listener approach is convenient if you want the duration of the
step scope to be the execution of the test method. For a more flexible,
but more invasive approach you can use the
<code class="classname">StepScopeTestUtils</code>. For example, to count the
number of items available in the reader above:</p><pre class="programlisting"><span class="hl-keyword">int</span> count = StepScopeTestUtils.doInStepScope(stepExecution,
<span class="hl-keyword">new</span> Callable&lt;Integer&gt;() {
<span class="hl-keyword">public</span> Integer call() <span class="hl-keyword">throws</span> Exception {
<span class="hl-keyword">int</span> count = <span class="hl-number">0</span>;
<span class="hl-keyword">while</span> (reader.read() != null) {
count++;
}
<span class="hl-keyword">return</span> count;
}
});</pre></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="validatingOutputFiles" href="#validatingOutputFiles"></a>10.5&nbsp;Validating Output Files</h2></div></div></div><p>When a batch job writes to the database, it is easy to query the
database to verify that the output is as expected. However, if the batch
job writes to a file, it is equally important that the output be verified.
Spring Batch provides a class <code class="classname">AssertFile</code> to
facilitate the verification of output files. The method
<code class="methodname">assertFileEquals</code> takes two
<code class="classname">File</code> objects (or two
<code class="classname">Resource</code> objects) and asserts, line by line, that
the two files have the same content. Therefore, it is possible to create a
file with the expected output and to compare it to the actual
result:</p><pre class="programlisting"><span class="hl-keyword">private</span> <span class="hl-keyword">static</span> <span class="hl-keyword">final</span> String EXPECTED_FILE = <span class="hl-string">"src/main/resources/data/input.txt"</span>;
<span class="hl-keyword">private</span> <span class="hl-keyword">static</span> <span class="hl-keyword">final</span> String OUTPUT_FILE = <span class="hl-string">"target/test-outputs/output.txt"</span>;
AssertFile.assertFileEquals(<span class="hl-keyword">new</span> FileSystemResource(EXPECTED_FILE),
<span class="hl-keyword">new</span> FileSystemResource(OUTPUT_FILE));</pre></div><div class="section"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="mockingDomainObjects" href="#mockingDomainObjects"></a>10.6&nbsp;Mocking Domain Objects</h2></div></div></div><p>Another common issue encountered while writing unit and integration
tests for Spring Batch components is how to mock domain objects. A good
example is a <code class="classname">StepExecutionListener</code>, as illustrated
below:</p><pre class="programlisting"><span class="hl-keyword">public</span> <span class="hl-keyword">class</span> NoWorkFoundStepExecutionListener <span class="hl-keyword">extends</span> StepExecutionListenerSupport {
<span class="hl-keyword">public</span> ExitStatus afterStep(StepExecution stepExecution) {
<span class="hl-keyword">if</span> (stepExecution.getReadCount() == <span class="hl-number">0</span>) {
<span class="hl-keyword">throw</span> <span class="hl-keyword">new</span> NoWorkFoundException(<span class="hl-string">"Step has not processed any items"</span>);
}
<span class="hl-keyword">return</span> stepExecution.getExitStatus();
}
}</pre><p>The above listener is provided by the framework and checks a
<code class="classname">StepExecution</code> for an empty read count, thus
signifying that no work was done. While this example is fairly simple, it
serves to illustrate the types of problems that may be encountered when
attempting to unit test classes that implement interfaces requiring Spring
Batch domain objects. Consider the above listener's unit test:</p><pre class="programlisting"><span class="hl-keyword">private</span> NoWorkFoundStepExecutionListener tested = <span class="hl-keyword">new</span> NoWorkFoundStepExecutionListener();
<em><span class="hl-annotation" style="color: gray">@Test</span></em>
<span class="hl-keyword">public</span> <span class="hl-keyword">void</span> testAfterStep() {
<span class="bold"><strong>StepExecution stepExecution = new StepExecution("NoProcessingStep",
new JobExecution(new JobInstance(1L, new JobParameters(),
"NoProcessingJob")));</strong></span>
stepExecution.setReadCount(<span class="hl-number">0</span>);
<span class="hl-keyword">try</span> {
tested.afterStep(stepExecution);
fail();
} <span class="hl-keyword">catch</span> (NoWorkFoundException e) {
assertEquals(<span class="hl-string">"Step has not processed any items"</span>, e.getMessage());
}
}</pre><p>Because the Spring Batch domain model follows good object orientated
principles, the StepExecution requires a
<code class="classname">JobExecution</code>, which requires a
<code class="classname">JobInstance</code> and
<code class="classname">JobParameters</code> in order to create a valid
<code class="classname">StepExecution</code>. While this is good in a solid domain
model, it does make creating stub objects for unit testing verbose. To
address this issue, the Spring Batch test module includes a factory for
creating domain objects: <code class="classname">MetaDataInstanceFactory</code>.
Given this factory, the unit test can be updated to be more
concise:</p><pre class="programlisting"><span class="hl-keyword">private</span> NoWorkFoundStepExecutionListener tested = <span class="hl-keyword">new</span> NoWorkFoundStepExecutionListener();
<em><span class="hl-annotation" style="color: gray">@Test</span></em>
<span class="hl-keyword">public</span> <span class="hl-keyword">void</span> testAfterStep() {
<span class="bold"><strong>StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution();</strong></span>
stepExecution.setReadCount(<span class="hl-number">0</span>);
<span class="hl-keyword">try</span> {
tested.afterStep(stepExecution);
fail();
} <span class="hl-keyword">catch</span> (NoWorkFoundException e) {
assertEquals(<span class="hl-string">"Step has not processed any items"</span>, e.getMessage());
}
}</pre><p>The above method for creating a simple
<code class="classname">StepExecution</code> is just one convenience method
available within the factory. A full method listing can be found in its
<a class="ulink" href="http://docs.spring.io/spring-batch/apidocs/org/springframework/batch/test/MetaDataInstanceFactory.html" target="_top">Javadoc</a>.</p></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="retry.html">Prev</a>&nbsp;</td><td width="20%" align="center">&nbsp;</td><td width="40%" align="right">&nbsp;<a accesskey="n" href="patterns.html">Next</a></td></tr><tr><td width="40%" align="left" valign="top">9.&nbsp;Retry&nbsp;</td><td width="20%" align="center"><a accesskey="h" href="index.html">Home</a></td><td width="40%" align="right" valign="top">&nbsp;11.&nbsp;Common Batch Patterns</td></tr></table></div></body></html>