component-runtime-junit
is a test library that allows you to validate simple logic based on the Talend Component Kit tooling.
To import it, add the following dependency to your project:
<dependency>
<groupId>org.talend.sdk.component</groupId>
<artifactId>component-runtime-junit</artifactId>
<version>${talend-component.version}</version>
<scope>test</scope>
</dependency>
This dependency also provides mocked components that you can use with your own component to create tests.
The mocked components are provided under the test
family:
-
emitter
: a mock of an input component -
collector
: a mock of an output component
Tip
|
the collector is by default "per thread". If you are executing a Beam (or concurrent) job it will not work.
To switch to a JVM wide storage you can set the system property talend.component.junit.handler.state to static (default being thread ).
This is likely done in a maven-surefire-plugin execution .
|
You can define a standard JUnit test and use the SimpleComponentRule
rule:
public class MyComponentTest {
@Rule (1)
public final SimpleComponentRule components = new SimpleComponentRule("org.talend.sdk.component.mycomponent");
@Test
public void produce() {
Job.components() (2)
.component("mycomponent","yourcomponentfamily://yourcomponent?"+createComponentConfig())
.component("collector", "test://collector")
.connections()
.from("mycomponent").to("collector")
.build()
.run();
final List<MyRecord> records = components.getCollectedData(MyRecord.class); (3)
doAssertRecords(records); // depending your test
}
}
-
The rule creates a component manager and provides two mock components: an emitter and a collector. Set the root package of your component to enable it.
-
Define any chain that you want to test. It generally uses the mock as source or collector.
-
Validate your component behavior. For a source, you can assert that the right records were emitted in the mock collect.
Tip
|
The rule can also be defined as a @ClassRule to start it once per class and not per test as with @Rule .
|
To go further, you can add the ServiceInjectionRule
rule, which allows to inject all the component family services into the test class by marking test class fields with @InjectService
:
public class SimpleComponentRuleTest {
@ClassRule
public static final SimpleComponentRule COMPONENT_FACTORY = new SimpleComponentRule("...");
@Rule (1)
public final ServiceInjectionRule injections = new ServiceInjectionRule(COMPONENT_FACTORY, this); (2)
@Service (3)
private LocalConfiguration configuration;
@Service
private Jsonb jsonb;
@Test
public void test() {
// ...
}
}
-
The injection requires the test instance, so it must be a
@Rule
rather than a@ClassRule
. -
The
ComponentsController
is passed to the rule, which for JUnit 4 is theSimpleComponentRule
, as well as the test instance to inject services in. -
All service fields are marked with
@Service
to let the rule inject them before the test is ran.
The JUnit 5 integration is very similar to JUnit 4, except that it uses the JUnit 5 extension mechanism.
The entry point is the @WithComponents
annotation that you add to your test class, and which takes the component package you want to test. You can use @Injected
to inject an instance of ComponentsHandler
- which exposes the same utilities than the JUnit 4 rule - in a test class field :
@WithComponents("org.talend.sdk.component.junit.component") (1)
public class ComponentExtensionTest {
@Injected (2)
private ComponentsHandler handler;
@Test
public void manualMapper() {
final Mapper mapper = handler.createMapper(Source.class, new Source.Config() {
{
values = asList("a", "b");
}
});
assertFalse(mapper.isStream());
final Input input = mapper.create();
assertEquals("a", input.next());
assertEquals("b", input.next());
assertNull(input.next());
}
}
-
The annotation defines which components to register in the test context.
-
The field allows to get the handler to be able to orchestrate the tests.
Note
|
If you use JUnit 5 for the first time, keep in mind that the imports changed and that you need to use org.junit.jupiter.api.Test instead of org.junit.Test .
Some IDE versions and surefire versions can also require you to install either a plugin or a specific configuration.
|
As for JUnit 4, you can go further by injecting test class fields marked with @InjectService
, but there is no additional extension to specify in this case:
@WithComponents("...")
class ComponentExtensionTest {
@Service (1)
private LocalConfiguration configuration;
@Service
private Jsonb jsonb;
@Test
void test() {
// ...
}
}
-
All service fields are marked with
@Service
to let the rule inject them before the test is ran.
Using the "test"/"collector" component as shown in the previous sample stores all records emitted by the chain (typically your source) in memory. You can then access them using theSimpleComponentRule.getCollectedData(type)
.
Note that this method filters by type. If you don’t need any specific type, you can use Object.class
.
The input mocking is symmetric to the output. In this case, you provide the data you want to inject:
public class MyComponentTest {
@Rule
public final SimpleComponentRule components = new SimpleComponentRule("org.talend.sdk.component.mycomponent");
@Test
public void produce() {
components.setInputData(asList(createData(), createData(), createData())); (1)
Job.components()
.component("emitter","test://emitter")
.component("out", "yourcomponentfamily://myoutput?"+createComponentConfig())
.connections()
.from("emitter").to("out")
.build
.run();
assertMyOutputProcessedTheInputData();
}
}
-
using
setInputData
, you prepare the execution(s) to have a fake input when using the "test"/"emitter" component.
The component configuration is a POJO (using @Option
on fields) and the runtime configuration (ExecutionChainBuilder
) uses a Map<String, String>
. To make the conversion easier, the JUnit integration provides a SimpleFactory.configurationByExample
utility to get this map instance from a configuration instance.
final MyComponentConfig componentConfig = new MyComponentConfig();
componentConfig.setUser("....");
// .. other inits
final Map<String, String> configuration = configurationByExample(componentConfig);
The same factory provides a fluent DSL to create the configuration by calling configurationByExample
without any parameter.
The advantage is to be able to convert an object as a Map<String, String>
or as a query string
in order to use it with the Job
DSL:
final String uri = "family://component?" +
configurationByExample().forInstance(componentConfig).configured().toQueryString();
It handles the encoding of the URI to ensure it is correctly done.
The SimpleComponentRule
also allows to test a mapper unitarily. You can get an instance from a configuration and execute this instance to collect the output.
public class MapperTest {
@ClassRule
public static final SimpleComponentRule COMPONENT_FACTORY = new SimpleComponentRule(
"org.company.talend.component");
@Test
public void mapper() {
final Mapper mapper = COMPONENT_FACTORY.createMapper(MyMapper.class, new Source.Config() {{
values = asList("a", "b");
}});
assertEquals(asList("a", "b"), COMPONENT_FACTORY.collectAsList(String.class, mapper));
}
}
As for a mapper, a processor is testable unitary. However, this case can be more complex in case of multiple inputs or outputs.
public class ProcessorTest {
@ClassRule
public static final SimpleComponentRule COMPONENT_FACTORY = new SimpleComponentRule(
"org.company.talend.component");
@Test
public void processor() {
final Processor processor = COMPONENT_FACTORY.createProcessor(Transform.class, null);
final SimpleComponentRule.Outputs outputs = COMPONENT_FACTORY.collect(processor,
new JoinInputFactory().withInput("__default__", asList(new Transform.Record("a"), new Transform.Record("bb")))
.withInput("second", asList(new Transform.Record("1"), new Transform.Record("2")))
);
assertEquals(2, outputs.size());
assertEquals(asList(2, 3), outputs.get(Integer.class, "size"));
assertEquals(asList("a1", "bb2"), outputs.get(String.class, "value"));
}
}
The rule allows you to instantiate a Processor
from your code, and then to collect
the output from the inputs you pass in. There are two convenient implementations of the input factory:
-
MainInputFactory
for processors using only the default input. -
JoinInputfactory
with thewithInput(branch, data)
method for processors using multiple inputs. The first argument is the branch name and the second argument is the data used by the branch.
Tip
|
If needed, you can also implement your own input representation using org.talend.sdk.component.junit.ControllableInputFactory .
|