Skip to content
This repository has been archived by the owner on Feb 25, 2022. It is now read-only.

Commit

Permalink
Merge pull request #8 in REP/willhaben-test-utils from feature/#11-te…
Browse files Browse the repository at this point in the history
…st-listener to master

* commit '4ba25c979e94504648fe3b460e548f091dc92743':
  #11 renamed module and package to maven-utils and maven.utils to make it more clear, what it is
  #11 junit is a compile dependency of course
  #11 added test scope to dependency
  #11 - added test listener, which generates a suite class into the target folder with exactly the same test class order as it was during the build
  • Loading branch information
Martin Lemanski committed Jul 6, 2017
2 parents 94e35a3 + 4ba25c9 commit 1d492ec
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 11 deletions.
4 changes: 2 additions & 2 deletions browserstack/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
<parent>
<groupId>at.willhaben.willtest</groupId>
<artifactId>parent</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>
</parent>

<artifactId>browserstack</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>
<packaging>jar</packaging>

<name>${project.groupId}:${project.artifactId}</name>
Expand Down
4 changes: 2 additions & 2 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
<parent>
<groupId>at.willhaben.willtest</groupId>
<artifactId>parent</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>
</parent>

<artifactId>core</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>
<packaging>jar</packaging>

<name>${project.groupId}:${project.artifactId}</name>
Expand Down
4 changes: 2 additions & 2 deletions examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
<parent>
<groupId>at.willhaben.willtest</groupId>
<artifactId>parent</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>
</parent>

<artifactId>examples</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>

<name>${project.groupId}:${project.artifactId}</name>
<description>Practical examples</description>
Expand Down
4 changes: 2 additions & 2 deletions log4j/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
<parent>
<groupId>at.willhaben.willtest</groupId>
<artifactId>parent</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>
</parent>

<artifactId>log4j</artifactId>
<version>1.1.1</version>
<version>1.1.2</version>

<name>${project.groupId}:${project.artifactId}</name>
<description>Log4j specific utilities</description>
Expand Down
75 changes: 75 additions & 0 deletions maven-utils/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>parent</artifactId>
<groupId>at.willhaben.willtest</groupId>
<version>1.1.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>maven-utils</artifactId>
<name>${project.groupId}:${project.artifactId}</name>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<scope>system</scope>
<version>${java.version}</version>
<systemPath>${toolsjar}</systemPath>
</dependency>
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-rules</artifactId>
<version>1.16.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-matchers</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
<profile>
<id>default-profile</id>
<activation>
<file>
<exists>${java.home}/../lib/tools.jar</exists>
</file>
</activation>
<properties>
<toolsjar>${java.home}/../lib/tools.jar</toolsjar>
</properties>
</profile>
<profile>
<id>mac-profile</id>
<activation>
<file>
<exists>${java.home}/../Classes/classes.jar</exists>
</file>
</activation>
<properties>
<toolsjar>${java.home}/../Classes/classes.jar</toolsjar>
</properties>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package at.willhaben.willtest.maven.utils;

import com.sun.codemodel.internal.*;
import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.junit.runner.notification.RunListener;
import org.junit.runners.Suite;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.*;

/**
* Generates a test suite class into maven target folder, which will contain the test classes in the execution order.<br/>
* The class will have the FQN {@value SUITE_TARGET_CLASS_NAME_PREFIX}{@value DATE_FORMAT}.<br/>
* The javadoc of the generated class will contain a table where the method order can be also seen.<br/>
* How to use it? See at the <a href="http://maven.apache.org/surefire/maven-surefire-plugin/examples/junit.html#Using_Custom_Listeners_and_Reporters">official surefire documentation site.</a>
*/
public class SuiteGeneratingRunListener extends RunListener {
static final String SUITE_TARGET_CLASS_NAME_PREFIX = "at.willhaben.willtest.generated.Suite";
private static final String MAVEN_MODULE_DIRECTORY_SYSTEM_PROPERTY = "basedir";
private static final String TARGET = "target";
private static final String VALUE_PARAM = "value";
public static final String DATE_FORMAT = "_yyyyMMdd_HHmmss";
private Map<String,String> importedTestClasses = new HashMap<>();

/**
* {@link LinkedHashMap} to preserve insertion order
*/
private LinkedHashMap<String,List<String>> testClassesAndTheirTests = new LinkedHashMap<>();

@Override
public synchronized void testStarted(Description description) throws Exception {
super.testStarted(description);
String className = description.getClassName();
List<String> testDescriptions = testClassesAndTheirTests.computeIfAbsent(className, key -> new ArrayList<>());
testDescriptions.add("Method '" + description.getMethodName() + "', DisplayName: '" + description.getDisplayName() + "'" );
}

@Override
public void testRunFinished(Result result) throws Exception {
super.testRunFinished(result);
JCodeModel codeModel = new JCodeModel();
JDefinedClass resultClass = codeModel._class(getSuiteClassName());
JClass suiteClazz = codeModel.ref(Suite.class);
resultClass.annotate(RunWith.class).param(VALUE_PARAM, suiteClazz);

JClass suiteClasses = codeModel.ref(Suite.SuiteClasses.class);

JAnnotationArrayMember testClassArray = resultClass.annotate(suiteClasses).paramArray(VALUE_PARAM);

testClassesAndTheirTests.keySet().forEach(className -> addClassToSuite(codeModel, testClassArray, className));

resultClass.javadoc().add(getJavaDocComment());

File file = new File(getTargetDirectory());
if ( !file.exists() && !file.mkdirs() ) {
throw new RuntimeException("Cannot create folder " + file.getAbsolutePath());
}
codeModel.build(file);
}

private void addClassToSuite(JCodeModel codeModel, JAnnotationArrayMember testClassArray, String className) {
try {
Class testClass = Class.forName(className);
String simpleName = testClass.getSimpleName();
if ( !importedTestClasses.containsKey(simpleName) ) {
JClass importedClass = codeModel.ref(testClass);
importedTestClasses.put(simpleName,className);
testClassArray.param(importedClass);
} else {
testClassArray.param(testClass);
}
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Cannot add class with name " + className + "!", e);
}
}

private String getJavaDocComment( ) {
StringBuilder sb = new StringBuilder();
sb.append("Test suite generated by ")
.append(SuiteGeneratingRunListener.class.getName())
.append(" based on execution order in surefire/failsafe maven plugin. " +
"Below you see the classes in order of execution and in the second column " +
"the test method order inside the class file.<br/>\n")
.append("<table>\n")
.append("<tr>\n\t<th>Test class</th><th>Test method and display name</th>\n</tr>\n");
testClassesAndTheirTests.forEach( (clazz, methods) -> sb.append(testClassToHTML(clazz, methods)));
sb.append("</table>");
return sb.toString();
}

private String testClassToHTML(String key, List<String> value) {
StringBuilder sb = new StringBuilder();
sb.append("<tr>\n");
if ( value.size() > 1) {
sb.append("\t<td rowspan=\"").append(value.size()).append("\">");
}
else {
sb.append("\t<td>");
}
sb.append(key).append("</td>");
for( int i = 0; i < value.size(); i++ ) {
if ( i == 0 ) {
sb.append("<td>").append(value.get(i)).append("</td>\n</tr>\n");
}
else {
sb.append("<tr>\n\t<td>").append(value.get(i)).append("</td>\n</tr>\n");
}
}
return sb.toString();
}

private String getTargetDirectory() {
return Optional.ofNullable(System.getProperty(MAVEN_MODULE_DIRECTORY_SYSTEM_PROPERTY)).orElse(".")
+ File.separator + TARGET;
}

private String getSuiteClassName() {
return SUITE_TARGET_CLASS_NAME_PREFIX + getTimeStamp();
}

private String getTimeStamp() {
return new SimpleDateFormat(DATE_FORMAT).format(getCurrentDate());
}

Date getCurrentDate() {
return new Date();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package at.willhaben.willtest.maven.utils;

import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.Description;
import org.junit.runner.Result;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import static com.jcabi.matchers.RegexMatchers.containsPattern;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.*;

public class SuiteGeneratingRunListenerTest {
private static String FIX_DATE = "20170211_142342";
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();

@Rule
public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();

private SuiteGeneratingRunListener suiteGeneratingRunListener;

@Before
public void prepare() throws ParseException {
ExposingSuiteGeneratingRunListener spy = spy(new ExposingSuiteGeneratingRunListener());
doReturn(new SimpleDateFormat("yyyyMMdd_HHmmss").parse(FIX_DATE))
.when(spy)
.getCurrentDate();
suiteGeneratingRunListener = spy;
System.setProperty("basedir", temporaryFolder.getRoot().getAbsolutePath());
}

@Test
public void test() throws Exception {
//for the sake of testing I just use simple jdk classes
suiteGeneratingRunListener.testStarted(
descriptionStubOf(Long.class.getName(), "methodA", "methodA[alma]"));
suiteGeneratingRunListener.testStarted(
descriptionStubOf(Long.class.getName(), "methodC", "methodC[alma]"));
//to test importing only one class in case of name conflict I use here 2 Date classes
suiteGeneratingRunListener.testStarted(
descriptionStubOf(java.util.Date.class.getName(), "methodB", "methodB[alma]"));
suiteGeneratingRunListener.testStarted(
descriptionStubOf(java.sql.Date.class.getName(), "methodBB", "methodBB[alma]"));
suiteGeneratingRunListener.testStarted(
descriptionStubOf(FileInputStream.class.getName(), "methodX", "methodX[alma]"));
suiteGeneratingRunListener.testRunFinished(mock(Result.class));
String name = SuiteGeneratingRunListener.SUITE_TARGET_CLASS_NAME_PREFIX.replace('.', File.separatorChar) + "_" + FIX_DATE + ".java";
File expectedTargetFile = new File(
temporaryFolder.getRoot().getAbsolutePath() + File.separator +
"target" + File.separator + name);
assertThat(expectedTargetFile.exists(),is(true));
String actualContent = convertWindowsNewLineToLinux(Files.toString(expectedTargetFile, StandardCharsets.UTF_8));
assertThat(
"Util date should be imported, since the first class is imported even if simple name collision happens.",
actualContent,
containsString("import java.util.Date;"));
assertThat(
"Sql date should not be imported, since util date is already imported.",
actualContent,
not(containsString("import java.sql.Date;")));
assertThat(
"Sql date should be in annotation with FQN present.",
actualContent,
containsPattern("\\sjava.sql.Date.class,"));
assertThat(
"Util date should be in annotation with simple name present.",
actualContent,
containsPattern("\\sDate.class,"));
try ( InputStream expectedFileAsStream = this.getClass().getResourceAsStream("/ExpectedSuite.java") ) {
String expectedFileContent = convertWindowsNewLineToLinux(
new String( ByteStreams.toByteArray(expectedFileAsStream), StandardCharsets.UTF_8 ));
assertThat(actualContent,is(expectedFileContent));
}
}

private String convertWindowsNewLineToLinux(String input) {
return input.replaceAll("\r\n", "\n");
}

private Description descriptionStubOf(String className, String methodName, String displayName) {
Description description = mock(Description.class);
when(description.getClassName()).thenReturn(className);
when(description.getMethodName()).thenReturn(methodName);
when(description.getDisplayName()).thenReturn(displayName);
return description;
}

private static class ExposingSuiteGeneratingRunListener extends SuiteGeneratingRunListener {
@Override
public Date getCurrentDate() {
return super.getCurrentDate();
}
}
}

0 comments on commit 1d492ec

Please sign in to comment.