39.e9699d7f.js 18.2 KB
Newer Older
茶陵後's avatar
茶陵後 已提交
1
(window.webpackJsonp=window.webpackJsonp||[]).push([[39],{467:function(e,t,n){"use strict";n.r(t);var a=n(56),s=Object(a.a)({},(function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("ContentSlotsDistributor",{attrs:{"slot-key":e.$parent.slotKey}},[n("h1",{attrs:{id:"unit-testing"}},[n("a",{staticClass:"header-anchor",attrs:{href:"#unit-testing"}},[e._v("#")]),e._v(" Unit Testing")]),e._v(" "),n("h2",{attrs:{id:"unit-testing-2"}},[n("a",{staticClass:"header-anchor",attrs:{href:"#unit-testing-2"}},[e._v("#")]),e._v(" Unit Testing")]),e._v(" "),n("p",[e._v("XMLJavaBoth")]),e._v(" "),n("p",[e._v("As with other application styles, it is extremely important to unit test any code written\nas part of a batch job. The Spring core documentation covers how to unit and integration\ntest with Spring in great detail, so it is not be repeated here. It is important, however,\nto think about how to 'end to end' test a batch job, which is what this chapter covers.\nThe spring-batch-test project includes classes that facilitate this end-to-end test\napproach.")]),e._v(" "),n("h3",{attrs:{id:"creating-a-unit-test-class"}},[n("a",{staticClass:"header-anchor",attrs:{href:"#creating-a-unit-test-class"}},[e._v("#")]),e._v(" Creating a Unit Test Class")]),e._v(" "),n("p",[e._v("In order for the unit test to run a batch job, the framework must load the job’s\nApplicationContext. Two annotations are used to trigger this behavior:")]),e._v(" "),n("ul",[n("li",[n("p",[n("code",[e._v("@RunWith(SpringJUnit4ClassRunner.class)")]),e._v(": Indicates that the class should use Spring’s\nJUnit facilities")])]),e._v(" "),n("li",[n("p",[n("code",[e._v("@ContextConfiguration(…​)")]),e._v(": Indicates which resources to configure the"),n("code",[e._v("ApplicationContext")]),e._v(" with.")])])]),e._v(" "),n("p",[e._v("Starting from v4.1, it is also possible to inject Spring Batch test utilities\nlike the "),n("code",[e._v("JobLauncherTestUtils")]),e._v(" and "),n("code",[e._v("JobRepositoryTestUtils")]),e._v(" in the test context\nusing the "),n("code",[e._v("@SpringBatchTest")]),e._v(" annotation.")]),e._v(" "),n("table",[n("thead",[n("tr",[n("th"),e._v(" "),n("th",[e._v("It should be noted that "),n("code",[e._v("JobLauncherTestUtils")]),e._v(" requires a "),n("code",[e._v("Job")]),e._v(" bean and that"),n("code",[e._v("JobRepositoryTestUtils")]),e._v(" requires a "),n("code",[e._v("DataSource")]),e._v(" bean. Since "),n("code",[e._v("@SpringBatchTest")]),e._v("registers a "),n("code",[e._v("JobLauncherTestUtils")]),e._v(" and a "),n("code",[e._v("JobRepositoryTestUtils")]),e._v(" in the test"),n("br"),e._v("context, it is expected that the test context contains a single autowire candidate"),n("br"),e._v("for a "),n("code",[e._v("Job")]),e._v(" and a "),n("code",[e._v("DataSource")]),e._v(" (either a single bean definition or one that is"),n("br"),e._v("annotated with "),n("code",[e._v("org.springframework.context.annotation.Primary")]),e._v(").")])])]),e._v(" "),n("tbody")]),e._v(" "),n("p",[e._v("The following Java example shows the annotations in use:")]),e._v(" "),n("p",[e._v("Using Java Configuration")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v("@SpringBatchTest\n@RunWith(SpringRunner.class)\n@ContextConfiguration(classes=SkipSampleConfiguration.class)\npublic class SkipSampleFunctionalTests { ... }\n")])])]),n("p",[e._v("The following XML example shows the annotations in use:")]),e._v(" "),n("p",[e._v("Using XML Configuration")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v('@SpringBatchTest\n@RunWith(SpringRunner.class)\n@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",\n                                    "/jobs/skipSampleJob.xml" })\npublic class SkipSampleFunctionalTests { ... }\n')])])]),n("h3",{attrs:{id:"end-to-end-testing-of-batch-jobs"}},[n("a",{staticClass:"header-anchor",attrs:{href:"#end-to-end-testing-of-batch-jobs"}},[e._v("#")]),e._v(" End-To-End Testing of Batch Jobs")]),e._v(" "),n("p",[e._v("'End To End' testing can be defined as testing the complete run of a batch job from\nbeginning to end. This allows for a test that sets up a test condition, executes the job,\nand verifies the end result.")]),e._v(" "),n("p",[e._v("Consider an example of a batch job that reads from the database and writes to a flat file.\nThe test method begins by setting up the database with test data. It clears the CUSTOMER\ntable and then inserts 10 new records. The test then launches the "),n("code",[e._v("Job")]),e._v(" by using the"),n("code",[e._v("launchJob()")]),e._v(" method. The "),n("code",[e._v("launchJob()")]),e._v(" method is provided by the "),n("code",[e._v("JobLauncherTestUtils")]),e._v("class. The "),n("code",[e._v("JobLauncherTestUtils")]),e._v(" class also provides the "),n("code",[e._v("launchJob(JobParameters)")]),e._v("method, which allows the test to give particular parameters. The "),n("code",[e._v("launchJob()")]),e._v(" method\nreturns the "),n("code",[e._v("JobExecution")]),e._v(" object, which is useful for asserting particular information\nabout the "),n("code",[e._v("Job")]),e._v(" run. In the following case, the test verifies that the "),n("code",[e._v("Job")]),e._v(' ended with\nstatus "COMPLETED".')]),e._v(" "),n("p",[e._v("The following listing shows the example in XML:")]),e._v(" "),n("p",[e._v("XML Based Configuration")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v('@SpringBatchTest\n@RunWith(SpringRunner.class)\n@ContextConfiguration(locations = { "/simple-job-launcher-context.xml",\n                                    "/jobs/skipSampleJob.xml" })\npublic class SkipSampleFunctionalTests {\n\n    @Autowired\n    private JobLauncherTestUtils jobLauncherTestUtils;\n\n    private SimpleJdbcTemplate simpleJdbcTemplate;\n\n    @Autowired\n    public void setDataSource(DataSource dataSource) {\n        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);\n    }\n\n    @Test\n    public void testJob() throws Exception {\n        simpleJdbcTemplate.update("delete from CUSTOMER");\n        for (int i = 1; i <= 10; i++) {\n            simpleJdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)",\n                                      i, "customer" + i);\n        }\n\n        JobExecution jobExecution = jobLauncherTestUtils.launchJob();\n\n        Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());\n    }\n}\n')])])]),n("p",[e._v("The following listing shows the example in Java:")]),e._v(" "),n("p",[e._v("Java Based Configuration")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v('@SpringBatchTest\n@RunWith(SpringRunner.class)\n@ContextConfiguration(classes=SkipSampleConfiguration.class)\npublic class SkipSampleFunctionalTests {\n\n    @Autowired\n    private JobLauncherTestUtils jobLauncherTestUtils;\n\n    private SimpleJdbcTemplate simpleJdbcTemplate;\n\n    @Autowired\n    public void setDataSource(DataSource dataSource) {\n        this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);\n    }\n\n    @Test\n    public void testJob() throws Exception {\n        simpleJdbcTemplate.update("delete from CUSTOMER");\n        for (int i = 1; i <= 10; i++) {\n            simpleJdbcTemplate.update("insert into CUSTOMER values (?, 0, ?, 100000)",\n                                      i, "customer" + i);\n        }\n\n        JobExecution jobExecution = jobLauncherTestUtils.launchJob();\n\n        Assert.assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());\n    }\n}\n')])])]),n("h3",{attrs:{id:"testing-individual-steps"}},[n("a",{staticClass:"header-anchor",attrs:{href:"#testing-individual-steps"}},[e._v("#")]),e._v(" Testing Individual Steps")]),e._v(" "),n("p",[e._v("For complex batch jobs, test cases in the end-to-end testing approach may become\nunmanageable. It these cases, it may be more useful to have test cases to test individual\nsteps on their own. The "),n("code",[e._v("AbstractJobTests")]),e._v(" class contains a method called "),n("code",[e._v("launchStep")]),e._v(",\nwhich takes a step name and runs just that particular "),n("code",[e._v("Step")]),e._v(". This approach allows for\nmore targeted tests letting the test set up data for only that step and to validate its\nresults directly. The following example shows how to use the "),n("code",[e._v("launchStep")]),e._v(" method to load a"),n("code",[e._v("Step")]),e._v(" by name:")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v('JobExecution jobExecution = jobLauncherTestUtils.launchStep("loadFileStep");\n')])])]),n("h3",{attrs:{id:"testing-step-scoped-components"}},[n("a",{staticClass:"header-anchor",attrs:{href:"#testing-step-scoped-components"}},[e._v("#")]),e._v(" Testing Step-Scoped Components")]),e._v(" "),n("p",[e._v("Often, the components that are configured for your steps at runtime use step scope and\nlate binding to inject context from the step or job execution. These are tricky to test as\nstandalone components, unless you have a way to set the context as if they were in a step\nexecution. That is the goal of two components in Spring Batch:"),n("code",[e._v("StepScopeTestExecutionListener")]),e._v(" and "),n("code",[e._v("StepScopeTestUtils")]),e._v(".")]),e._v(" "),n("p",[e._v("The listener is declared at the class level, and its job is to create a step execution\ncontext for each test method, as shown in the following example:")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v('@ContextConfiguration\n@TestExecutionListeners( { DependencyInjectionTestExecutionListener.class,\n    StepScopeTestExecutionListener.class })\n@RunWith(SpringRunner.class)\npublic class StepScopeTestExecutionListenerIntegrationTests {\n\n    // This component is defined step-scoped, so it cannot be injected unless\n    // a step is active...\n    @Autowired\n    private ItemReader<String> reader;\n\n    public StepExecution getStepExecution() {\n        StepExecution execution = MetaDataInstanceFactory.createStepExecution();\n        execution.getExecutionContext().putString("input.data", "foo,bar,spam");\n        return execution;\n    }\n\n    @Test\n    public void testReader() {\n        // The reader is initialized and bound to the input data\n        assertNotNull(reader.read());\n    }\n\n}\n')])])]),n("p",[e._v("There are two "),n("code",[e._v("TestExecutionListeners")]),e._v(". One is the regular Spring Test framework, which\nhandles dependency injection from the configured application context to inject the reader.\nThe other is the Spring Batch "),n("code",[e._v("StepScopeTestExecutionListener")]),e._v(". It works by looking for a\nfactory method in the test case for a "),n("code",[e._v("StepExecution")]),e._v(", using that as the context for the\ntest method, as if that execution were active in a "),n("code",[e._v("Step")]),e._v(" at runtime. The factory method\nis detected by its signature (it must return a "),n("code",[e._v("StepExecution")]),e._v("). If a factory method is\nnot provided, then a default "),n("code",[e._v("StepExecution")]),e._v(" is created.")]),e._v(" "),n("p",[e._v("Starting from v4.1, the "),n("code",[e._v("StepScopeTestExecutionListener")]),e._v(" and"),n("code",[e._v("JobScopeTestExecutionListener")]),e._v(" are imported as test execution listeners\nif the test class is annotated with "),n("code",[e._v("@SpringBatchTest")]),e._v(". The preceding test\nexample can be configured as follows:")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v('@SpringBatchTest\n@RunWith(SpringRunner.class)\n@ContextConfiguration\npublic class StepScopeTestExecutionListenerIntegrationTests {\n\n    // This component is defined step-scoped, so it cannot be injected unless\n    // a step is active...\n    @Autowired\n    private ItemReader<String> reader;\n\n    public StepExecution getStepExecution() {\n        StepExecution execution = MetaDataInstanceFactory.createStepExecution();\n        execution.getExecutionContext().putString("input.data", "foo,bar,spam");\n        return execution;\n    }\n\n    @Test\n    public void testReader() {\n        // The reader is initialized and bound to the input data\n        assertNotNull(reader.read());\n    }\n\n}\n')])])]),n("p",[e._v("The listener approach is convenient if you want the duration of the step scope to be the\nexecution of the test method. For a more flexible but more invasive approach, you can use\nthe "),n("code",[e._v("StepScopeTestUtils")]),e._v(". The following example counts the number of items available in\nthe reader shown in the previous example:")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v("int count = StepScopeTestUtils.doInStepScope(stepExecution,\n    new Callable<Integer>() {\n      public Integer call() throws Exception {\n\n        int count = 0;\n\n        while (reader.read() != null) {\n           count++;\n        }\n        return count;\n    }\n});\n")])])]),n("h3",{attrs:{id:"validating-output-files"}},[n("a",{staticClass:"header-anchor",attrs:{href:"#validating-output-files"}},[e._v("#")]),e._v(" Validating Output Files")]),e._v(" "),n("p",[e._v("When a batch job writes to the database, it is easy to query the database to verify that\nthe output is as expected. However, if the batch job writes to a file, it is equally\nimportant that the output be verified. Spring Batch provides a class called "),n("code",[e._v("AssertFile")]),e._v("to facilitate the verification of output files. The method called "),n("code",[e._v("assertFileEquals")]),e._v(" takes\ntwo "),n("code",[e._v("File")]),e._v(" objects (or two "),n("code",[e._v("Resource")]),e._v(" objects) and asserts, line by line, that the two\nfiles have the same content. Therefore, it is possible to create a file with the expected\noutput and to compare it to the actual result, as shown in the following example:")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v('private static final String EXPECTED_FILE = "src/main/resources/data/input.txt";\nprivate static final String OUTPUT_FILE = "target/test-outputs/output.txt";\n\nAssertFile.assertFileEquals(new FileSystemResource(EXPECTED_FILE),\n                            new FileSystemResource(OUTPUT_FILE));\n')])])]),n("h3",{attrs:{id:"mocking-domain-objects"}},[n("a",{staticClass:"header-anchor",attrs:{href:"#mocking-domain-objects"}},[e._v("#")]),e._v(" Mocking Domain Objects")]),e._v(" "),n("p",[e._v("Another common issue encountered while writing unit and integration tests for Spring Batch\ncomponents is how to mock domain objects. A good example is a "),n("code",[e._v("StepExecutionListener")]),e._v(", as\nillustrated in the following code snippet:")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v("public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport {\n\n    public ExitStatus afterStep(StepExecution stepExecution) {\n        if (stepExecution.getReadCount() == 0) {\n            return ExitStatus.FAILED;\n        }\n        return null;\n    }\n}\n")])])]),n("p",[e._v("The preceding listener example is provided by the framework and checks a "),n("code",[e._v("StepExecution")]),e._v("for an empty read count, thus signifying that no work was done. While this example is\nfairly simple, it serves to illustrate the types of problems that may be encountered when\nattempting to unit test classes that implement interfaces requiring Spring Batch domain\nobjects. Consider the following unit test for the listener’s in the preceding example:")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v('private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener();\n\n@Test\npublic void noWork() {\n    StepExecution stepExecution = new StepExecution("NoProcessingStep",\n                new JobExecution(new JobInstance(1L, new JobParameters(),\n                                 "NoProcessingJob")));\n\n    stepExecution.setExitStatus(ExitStatus.COMPLETED);\n    stepExecution.setReadCount(0);\n\n    ExitStatus exitStatus = tested.afterStep(stepExecution);\n    assertEquals(ExitStatus.FAILED.getExitCode(), exitStatus.getExitCode());\n}\n')])])]),n("p",[e._v("Because the Spring Batch domain model follows good object-oriented principles, the"),n("code",[e._v("StepExecution")]),e._v(" requires a "),n("code",[e._v("JobExecution")]),e._v(", which requires a "),n("code",[e._v("JobInstance")]),e._v(" and"),n("code",[e._v("JobParameters")]),e._v(", to create a valid "),n("code",[e._v("StepExecution")]),e._v(". While this is good in a solid domain\nmodel, it does make creating stub objects for unit testing verbose. To address this issue,\nthe Spring Batch test module includes a factory for creating domain objects:"),n("code",[e._v("MetaDataInstanceFactory")]),e._v(". Given this factory, the unit test can be updated to be more\nconcise, as shown in the following example:")]),e._v(" "),n("div",{staticClass:"language- extra-class"},[n("pre",{pre:!0,attrs:{class:"language-text"}},[n("code",[e._v("private NoWorkFoundStepExecutionListener tested = new NoWorkFoundStepExecutionListener();\n\n@Test\npublic void testAfterStep() {\n    StepExecution stepExecution = MetaDataInstanceFactory.createStepExecution();\n\n    stepExecution.setExitStatus(ExitStatus.COMPLETED);\n    stepExecution.setReadCount(0);\n\n    ExitStatus exitStatus = tested.afterStep(stepExecution);\n    assertEquals(ExitStatus.FAILED.getExitCode(), exitStatus.getExitCode());\n}\n")])])]),n("p",[e._v("The preceding method for creating a simple "),n("code",[e._v("StepExecution")]),e._v(" is just one convenience method\navailable within the factory. A full method listing can be found in its"),n("a",{attrs:{href:"https://docs.spring.io/spring-batch/apidocs/org/springframework/batch/test/MetaDataInstanceFactory.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Javadoc"),n("OutboundLink")],1),e._v(".")])])}),[],!1,null,null,null);t.default=s.exports}}]);