Skip to content

Commit

Permalink
Dynamic container junit5 (#943)
Browse files Browse the repository at this point in the history
* Group chained flows into dynamic containers in junit 5

* Code refactor to maintain existing test execution order

* Add junit tests

* Group chained flows for junit5 Flocessor based on chainId

* Use chain tag for display name of the DynamicContainer

---------

Co-authored-by: Chaitanya Srinidhi V <ChaitanyaSrinidhi.V@mastercard.com>
Co-authored-by: Ryan McNally <ryan.mcnally@mastercard.com>
  • Loading branch information
3 people authored Sep 30, 2024
1 parent 6234c85 commit 02d3cf2
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
package com.mastercard.test.flow.assrt.junit5;

import static org.junit.jupiter.api.DynamicTest.dynamicTest;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.opentest4j.IncompleteExecutionException;
import org.opentest4j.TestAbortedException;

import static com.mastercard.test.flow.assrt.Order.CHAIN_TAG_PREFIX;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

import com.mastercard.test.flow.Flow;
import com.mastercard.test.flow.Model;
import com.mastercard.test.flow.assrt.AbstractFlocessor;
import com.mastercard.test.flow.assrt.History.Result;
import com.mastercard.test.flow.util.Tags;

/**
* Integrates {@link Flow} processing into junit 5. This should be used as the
Expand Down Expand Up @@ -53,33 +60,88 @@ public Flocessor( String title, Model model ) {
* @return A stream of test cases
*/
public Stream<DynamicNode> tests() {
return flows().map( flow -> dynamicTest(
flow.meta().id(),
testSource( flow ),
() -> {
try {
process( flow );
history.recordResult( flow, Result.SUCCESS );
}
catch( IncompleteExecutionException iee ) {
// not strictly required to record the skipped outcome in the history, as it
// does not inform the processing of later flows. That may change in the future
// though, so for now we're going to live with the mutation testing complaint
history.recordResult( flow, Result.SKIP );
throw iee;
}
catch( AssertionError ae ) {
history.recordResult( flow, Result.UNEXPECTED );
throw ae;
}
catch( Exception e ) {
// not strictly required to record the error outcome in the history, as it
// does not inform the processing of later flows. That may change in the future
// though, so for now we're going to live with the mutation testing complaint
history.recordResult( flow, Result.ERROR );
throw e;
}
} ) );
List<DynamicNode> nodes = new ArrayList<>();
List<Flow> currentChain = new ArrayList<>();
String currentChainId = null;

// Iterate over flows once and separate them into chained and non-chained
for( Flow flow : flows().collect( Collectors.toList() ) ) {
Optional<String> chainSuffix = Tags.suffix( flow.meta().tags(), CHAIN_TAG_PREFIX );
if( chainSuffix.isPresent() ) {
String chainId = chainSuffix.get();
if( currentChainId == null || currentChainId.equals( chainId ) ) {
currentChainId = chainId;
currentChain.add( flow );
}
else {
// End of the previous chain, add the current chain to nodes
nodes.add( createDynamicContainer( currentChain ) );
currentChain.clear();
currentChainId = chainId;
currentChain.add( flow );
}
}
else {
if( currentChainId != null ) {
// End of the current chain, add the current chain to nodes
nodes.add( createDynamicContainer( currentChain ) );
currentChain.clear();
currentChainId = null;
}
nodes.add( dynamicTest(
flow.meta().id(),
testSource( flow ),
() -> processFlow( flow ) ) );
}
}

// If the last flows were part of a chain, add them as well
if( !currentChain.isEmpty() ) {
nodes.add( createDynamicContainer( currentChain ) );
}
return nodes.stream();
}

private void processFlow( Flow flow ) {
try {
process( flow );
history.recordResult( flow, Result.SUCCESS );
}
catch( IncompleteExecutionException iee ) {
// not strictly required to record the skipped outcome in the history, as it
// does not inform the processing of later flows. That may change in the future
// though, so for now we're going to live with the mutation testing complaint
history.recordResult( flow, Result.SKIP );
throw iee;
}
catch( AssertionError ae ) {
history.recordResult( flow, Result.UNEXPECTED );
throw ae;
}
catch( Exception e ) {
// not strictly required to record the error outcome in the history, as it
// does not inform the processing of later flows. That may change in the future
// though, so for now we're going to live with the mutation testing complaint
history.recordResult( flow, Result.ERROR );
throw e;
}
}

private DynamicContainer createDynamicContainer( List<Flow> chain ) {
List<DynamicTest> tests = chain.stream()
.map( flow -> dynamicTest(
flow.meta().id(),
testSource( flow ),
() -> processFlow( flow ) ) )
.collect( Collectors.toList() );

// Use the chain tag for the display name
String chainTag = chain.stream()
.findFirst()
.flatMap( flow -> Tags.suffix( flow.meta().tags(), CHAIN_TAG_PREFIX ) )
.orElse( chain.get( 0 ).meta().id() );

return DynamicContainer.dynamicContainer( "chain:" + chainTag, tests );
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.mastercard.test.flow.assrt.junit5;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.mastercard.test.flow.Flow;
import com.mastercard.test.flow.Model;
import com.mastercard.test.flow.builder.Chain;
import com.mastercard.test.flow.builder.Creator;
import com.mastercard.test.flow.util.Tags;

/**
* Validates the {@link Flocessor} class for DynamicContainer creation of
* chained flows
*/
@SuppressWarnings("static-method")
class FlocessorTest {

/**
* A simple sequence of flows with no chains
*/
@Test
void simple() {
expectNodes( model( null, null, null, null, null ),
"test : 0 []",
"test : 1 []",
"test : 2 []",
"test : 3 []",
"test : 4 []" );
}

/**
* A single chain of a single flow
*/
@Test
void link() {
expectNodes( model( null, "a", null ),
"test : 0 []",
"container : chain:a",
" test : 1 [chain:a]",
"test : 2 []" );
}

/**
* Consecutive single-flow chains
*/
@Test
void links() {
expectNodes( model( "a", "b", "c" ),
"container : chain:a",
" test : 0 [chain:a]",
"container : chain:b",
" test : 1 [chain:b]",
"container : chain:c",
" test : 2 [chain:c]" );
}

/**
* A single multi-flow chain
*/
@Test
void chain() {
// in the middle
expectNodes( model( null, "a", "a", "a", null ),
"test : 0 []",
"container : chain:a",
" test : 1 [chain:a]",
" test : 2 [chain:a]",
" test : 3 [chain:a]",
"test : 4 []" );

// at the start
expectNodes( model( "a", "a", "a", null ),
"container : chain:a",
" test : 0 [chain:a]",
" test : 1 [chain:a]",
" test : 2 [chain:a]",
"test : 3 []" );

// at the end
expectNodes( model( null, "a", "a", "a" ),
"test : 0 []",
"container : chain:a",
" test : 1 [chain:a]",
" test : 2 [chain:a]",
" test : 3 [chain:a]" );
}

/**
* Multiple multi-flow chains
*/
@Test
void chains() {
expectNodes( model( "a", "a", null, "b", "b", "c" ),
"container : chain:a",
" test : 0 [chain:a]",
" test : 1 [chain:a]",
"test : 2 []",
"container : chain:b",
" test : 3 [chain:b]",
" test : 4 [chain:b]",
"container : chain:c",
" test : 5 [chain:c]" );
}

private static Model model( String... chains ) {
List<Flow> flows = new ArrayList<>();
for( int i = 0; i < chains.length; i++ ) {
int idx = i;
Flow flow = Creator
.build( f -> f.meta( data -> data
.description( String.valueOf( idx ) )
.tags( Tags.add( Optional.ofNullable( chains[idx] )
.map( v -> Chain.PREFIX + v )
.orElse( "" ) ) ) ) );
flows.add( flow );
}
Model model = mock( Model.class );
when( model.flows( anySet(), anySet() ) )
.thenReturn( flows.stream() );

return model;
}

private static void expectNodes( Model model, String... expected ) {
Flocessor flocessor = new Flocessor( "", model );
List<String> actual = new ArrayList<>();
flocessor.tests()
.forEach( node -> stringify( node, "", actual ) );
assertEquals(
copypasta( Stream.of( expected ) ),
copypasta( actual.stream() ) );
}

private static void stringify( DynamicNode node, String prefix, List<String> lines ) {
if( node instanceof DynamicTest ) {
DynamicTest test = (DynamicTest) node;
lines.add( prefix + "test : " + test.getDisplayName() );
}
else if( node instanceof DynamicContainer ) {
DynamicContainer container = (DynamicContainer) node;
lines.add( prefix + "container : " + container.getDisplayName() );
container.getChildren()
.forEach( child -> stringify( child, prefix + " ", lines ) );
}
else {
throw new IllegalStateException( "unexpected node " + node.getClass() );
}
}

/**
* @param content Some strings
* @return A string that can be trivially copy/pasted into java source
*/
private static String copypasta( Stream<String> content ) {
return content
.map( s -> s.replaceAll( "\r", "" ) )
.flatMap( s -> Stream.of( s.split( "\n" ) ) )
.map( s -> s.replaceAll( "\"", "'" ) )
.collect( Collectors.joining( "\",\n\"", "\"", "\"" ) );
}
}
7 changes: 7 additions & 0 deletions assert/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
<dependencyManagement>
<dependencies />
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
Expand Down

0 comments on commit 02d3cf2

Please sign in to comment.