Skip to content
This repository was archived by the owner on Dec 5, 2020. It is now read-only.

Commit 854f586

Browse files
leifgehrmannLeif Gehrmann
authored andcommitted
First version
0 parents  commit 854f586

21 files changed

+6545
-0
lines changed

.eslintrc.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"env": {
3+
"node": true,
4+
"es6": true,
5+
"jest": true
6+
},
7+
"parserOptions": {
8+
"ecmaVersion": 2017
9+
},
10+
"extends": "airbnb-base"
11+
}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea
2+
node_modules
3+
coverage
4+
tests/testCases/actual

ParameterizedTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package x.x.x;
2+
3+
import org.junit.Test;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Target;
7+
8+
@Target({ElementType.METHOD})
9+
public @interface ParameterizedTest {
10+
Class<? extends Throwable> expected() default Test.None.class;
11+
}

README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# junit4-parameterized-tests-generator
2+
3+
[![Build Status](https://github.com/leifgehrmann/junit4-parameterized-tests-generator/workflows/Tests/badge.svg?branch=master)](https://github.com/leifgehrmann/junit4-parameterized-tests-generator/actions)
4+
5+
Script to generate parameterized JUnit-4 tests for test runners that do not
6+
support parameterized tests.
7+
8+
## Purpose
9+
10+
JUnit-4 has a design flaw where [test runners] cannot be combined to perform
11+
multiple behaviours. For example, JUnit's [`Parameterized`] test runner cannot
12+
be combined with [`RobolectricTestRunner`], or any other test runner because
13+
only one `@RunWith` annotation is permitted per class.
14+
15+
[test runners]: https://github.com/junit-team/junit4/wiki/Test-runners
16+
[`Parameterized`]: https://github.com/junit-team/junit4/wiki/Parameterized-tests
17+
[`RobolectricTestRunner`]: http://robolectric.org
18+
19+
`generateParameterizedTests.js` solves this by parsing special comments in the
20+
test...
21+
22+
```java
23+
class StepUnitFormatterTests {
24+
//> Zero(0, "0 steps")
25+
//> Singular(1, "1 step")
26+
//> Plural(2, "2 steps")
27+
//> 999(999, "999 steps")
28+
//> Thousand(1000, "1,000 steps")
29+
//> Million(1000000, "1,000,000 steps")
30+
//> Decimal(3.3, "3 steps")
31+
//> Negative(-5, "0 steps")
32+
@ParameterizedTest
33+
public void testFormat(double numberOfSteps, String expected) {
34+
Assert.assertEquals(expected, StepUnitFormatter().format(numberOfSteps));
35+
}
36+
}
37+
```
38+
39+
...and converting them to individual tests, which get appended to the end of
40+
the file like this:
41+
42+
```java
43+
// DO NOT EDIT BELOW THIS LINE
44+
45+
@Test
46+
public void testFormatZero() {
47+
testFormat(0, "0 steps")
48+
}
49+
50+
@Test
51+
public void testFormatSingular() {
52+
testFormat(1, "1 step")
53+
}
54+
55+
@Test
56+
public void testFormatPlural() {
57+
testFormat(2, "2 steps")
58+
}
59+
60+
// Etc.
61+
}
62+
```
63+
64+
## Installation
65+
66+
1. Copy `generateParameterizedTests.js` anywhere into your Java project.
67+
2. Copy `ParameterizedTest.java` into your test source code folder.
68+
3. Update `package x.x.x;` in `ParameterizedTest.java` to reflect your
69+
project's package structure.
70+
71+
## Running
72+
73+
Node must be installed, but `npm install` is not required. To generate the
74+
parameterized tests, run the following:
75+
76+
```shell script
77+
$ node generateParameterizedTests.js path/to/tests
78+
```
79+
80+
### Running automatically using Gradle
81+
82+
To avoid having to run the generator manually, a task can be added to your
83+
`build.gradle` file to run the script on `preBuild`, or any other preferred
84+
gradle task.
85+
86+
```gradle
87+
task generateParameterizedTests(type: Exec) {
88+
executable "sh"
89+
args "-c", "node scripts/generateParameterizedTests.js src/test"
90+
}
91+
92+
preBuild.dependsOn generateParameterizedTests
93+
```
94+
95+
## Attribution
96+
97+
This code is dedicated to the public domain, so no attribution is needed.
98+
99+
This script is based on [a similar implementation by Elliot Chance for
100+
generating parameterized tests in Swift](
101+
https://medium.com/@elliotchance/parameterized-data-driven-tests-in-swift-3b9a46891801).

UNLICENSE

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <https://unlicense.org>

generateParameterizedTests.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
/**
5+
* Returns an array of files (absolute path) matching a file extension in a
6+
* directory.
7+
*
8+
* @param {string} directory
9+
* @param {string} extension
10+
*
11+
* @returns {string[]}
12+
*/
13+
function getFilesWithExtension(directory, extension) {
14+
const matchedFiles = [];
15+
16+
if (!fs.existsSync(directory)) {
17+
throw new Error(`Directory "${directory}" does not exist`);
18+
}
19+
20+
const files = fs.readdirSync(directory);
21+
for (let i = 0; i < files.length; i += 1) {
22+
const filename = path.join(directory, files[i]);
23+
const stat = fs.lstatSync(filename);
24+
if (stat.isDirectory()) {
25+
// Recurse
26+
const nestedMatchedFiles = getFilesWithExtension(filename, extension);
27+
matchedFiles.push(...nestedMatchedFiles);
28+
} else if (filename.endsWith(extension)) {
29+
matchedFiles.push(filename);
30+
}
31+
}
32+
33+
return matchedFiles;
34+
}
35+
36+
/**
37+
* @param {string} line
38+
*
39+
* @returns {null|{testCaseName: string, testCaseArguments: string}}
40+
*/
41+
function parseParameterizedTestCase(line) {
42+
const regex = /^\s*\/\/>\s*(\w*)(.*)\s*$/m;
43+
const matches = regex.exec(line);
44+
if (matches === null) {
45+
return null;
46+
}
47+
return {
48+
testCaseName: matches[1],
49+
testCaseArguments: matches[2],
50+
};
51+
}
52+
53+
/**
54+
* @param {string} line
55+
*
56+
* @returns {null|string}
57+
*/
58+
function parseParameterizedTestAnnotation(line) {
59+
const regex = /^\s*@ParameterizedTest(.*)$/m;
60+
const matches = regex.exec(line);
61+
if (matches === null) {
62+
return null;
63+
}
64+
return matches[1];
65+
}
66+
67+
/**
68+
* @param {string} line
69+
*
70+
* @returns {null|string}
71+
*/
72+
function parseTestFunction(line) {
73+
const regex = /^\s*public\s*void\s*([a-zA-Z0-9_]*)(.*)$/m;
74+
const matches = regex.exec(line);
75+
if (matches === null) {
76+
return null;
77+
}
78+
return matches[1];
79+
}
80+
81+
/**
82+
* @param {string} testName
83+
* @param {string} testAnnotation
84+
* @param {{testCaseName: string, testCaseArguments: string}[]} testCases
85+
*
86+
* @returns {string[]}
87+
*/
88+
function generateTestsForParameterizedTest(testName, testAnnotation, testCases) {
89+
return testCases.map((testCase) => `
90+
@Test${testAnnotation}
91+
public void ${testName + testCase.testCaseName}() {
92+
${testName + testCase.testCaseArguments};
93+
}`);
94+
}
95+
96+
/**
97+
* @param {string} filepath
98+
*/
99+
function processFile(filepath) {
100+
const dividingLine = '\n // DO NOT EDIT BELOW THIS LINE\n';
101+
const generatedTests = [];
102+
103+
// Keep track of various tokens while processing the file
104+
let parameterizedTestCases = [];
105+
let parameterizedTestAnnotation = null;
106+
107+
// Read contents of the file
108+
const fileContent = fs.readFileSync(filepath, 'UTF-8');
109+
const lines = fileContent.split(/\r?\n/);
110+
111+
// For each line in the file
112+
lines.forEach((line) => {
113+
// Find parameterized test cases
114+
const parsedParameterizedTestCase = parseParameterizedTestCase(line);
115+
if (parsedParameterizedTestCase !== null) {
116+
parameterizedTestCases.push(parsedParameterizedTestCase);
117+
return;
118+
}
119+
120+
// Find parameterized test annotations, which may include "expected=..."
121+
// attributes
122+
const parsedParameterizedTestAnnotation = parseParameterizedTestAnnotation(
123+
line,
124+
);
125+
if (parsedParameterizedTestAnnotation !== null) {
126+
parameterizedTestAnnotation = parsedParameterizedTestAnnotation;
127+
return;
128+
}
129+
130+
// Find the parameterized test name
131+
const parsedParameterizedTestName = parseTestFunction(line);
132+
if (parsedParameterizedTestName !== null) {
133+
if (
134+
parameterizedTestCases.length > 0
135+
&& parameterizedTestAnnotation !== null
136+
) {
137+
generatedTests.push(
138+
...generateTestsForParameterizedTest(
139+
parsedParameterizedTestName,
140+
parameterizedTestAnnotation,
141+
parameterizedTestCases,
142+
),
143+
);
144+
}
145+
parameterizedTestCases = [];
146+
parameterizedTestAnnotation = null;
147+
}
148+
});
149+
150+
// If no parameterized tests were found, do not change anything
151+
if (generatedTests.length === 0) {
152+
return;
153+
}
154+
155+
// Determine where to append the generated tests
156+
let newFileContent;
157+
const dividingLinePosition = fileContent.indexOf(dividingLine);
158+
if (dividingLinePosition === -1) {
159+
// Find the last '}' at the end of the file
160+
newFileContent = fileContent.replace(/\s*}\s*$/g, '\n');
161+
} else {
162+
// Find the dividing line position
163+
newFileContent = fileContent.substring(0, dividingLinePosition);
164+
}
165+
166+
// Append generated tests to end of file
167+
newFileContent += dividingLine;
168+
newFileContent += generatedTests.join('\n');
169+
newFileContent += '\n}\n';
170+
fs.writeFileSync(filepath, newFileContent);
171+
}
172+
173+
const testDir = `${process.cwd()}/${process.argv[2]}`;
174+
getFilesWithExtension(testDir, '.java').forEach((filepath) => {
175+
processFile(filepath);
176+
});

jestconfig.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"testRegex": "/tests/.*\\.(test|spec)?\\.(js|jsx)$",
3+
"collectCoverage": true,
4+
"collectCoverageFrom": ["generateParameterizedTests.js"]
5+
}

0 commit comments

Comments
 (0)