component-runtime-junit
component-runtime-junit
is a small test library allowing you to validate simple logic based on Talend Component tooling.
To import it add to your project the following dependency:
<dependency>
<groupId>org.talend.sdk.component</groupId>
<artifactId>component-runtime-junit</artifactId>
<version>${talend-component.version}</version>
<scope>test</scope>
</dependency>
This dependency also provide some mocked components that you can use with your own component to create tests.
The mocked components are provided under the family test
:
-
emitter
: a mock of an input component -
collector
: a mock of an output component
JUnit 4
Then 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
}
}
1 | the rule will create a component manager and provide two mock components: an emitter and a collector. Don’t forget to set the root package of your component to enable it. |
2 | you define any chain you want to test, it generally uses the mock as source or collector |
3 | you validate your component behavior, for a source you can assert the right records were emitted in the mock collect |
JUnit 5
The JUnit 5 integration is mainly the same as for JUnit 4 except it uses the new JUnit 5 extension mecanism.
The entry point is the @WithComponents
annotation you put on your test class which takes the
component package you want to test and you can use @Injected
to inject in a test class field an instance of ComponentsHandler
which exposes the same utilities than the JUnit 4 rule:
@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());
}
}
1 | The annotation defines which components to register in the test context. |
2 | The field allows to get the handler to be able to orchestrate the tests. |
if it is the first time you use JUnit 5, don’t forget the imports changed and you must use org.junit.jupiter.api.Test instead of org.junit.Test .
Some IDE versions and surefire versions can also need you to install either a plugin or a specific configuration.
|
Mocking the output
Using the component "test"/"collector" as in 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 care of the type just use Object.class
.
Mocking the input
The input mocking is symmetric to the output but here 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();
}
}
1 | using setInputData you prepare the execution(s) to have a fake input when using "test"/"emitter" component. |
Creating runtime configuration from component configuration
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.
Example:
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 configuration calling configurationByExample
without any parameter.
The advantage is to be able to convert an object as a Map<String, String>
as seen previously or as a query string
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.
Testing a Mapper
The SimpleComponentRule
also allows to test a mapper unitarly, you can get an instance from a configuration
and you can execute this instance to collect the output. Here is a snippet doing that:
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));
}
}
Testing a Processor
As for the mapper a processor is testable unitary. The case is a bit more complex since you can have multiple inputs and 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"));
}
}
Here again 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 implementation
of the input factory:
-
MainInputFactory
for processors using only the default input. -
JoinInputfactory
for processors using multiple inputs have a methodwithInput(branch, data)
The first arg is the branch name and the second arg is the data used by the branch.
you can also implement your own input representation if needed implementing org.talend.sdk.component.junit.ControllableInputFactory .
|