How I implemented Java Code Testing without any build automation tool for my school's club
As a core member of the open-source club at scaler school of technology, I was tasked to write a GitHub workflow that would test contributor's solution for a given DSA problem.
The Desired Folder Structure
This was the folder structure I had in mind. First I thought I should go for a testing library in Java, so I scoured the internet and found junit5.
The problem with testing libraries
Testing libraries are fairly easy to set up and plug into GitHub workflows, but there's one major flaw, they are only customizable to some extent.
The desired flow I had in mind was ~
This was somewhat achievable with testing libraries, but they require build automation tools like Maven, which means that the folder structures would become more complicated and I wanted to keep it very simple so that my fellow batchmates wouldn't get confused too much.
My Solution to the problem
I then decided that I would write a custom GitHub workflow to tackle this problem.
Just look for file changes inside javaProblems
directory, see which file was changed and run the test file in that file's folder. It should be pretty simple, huh?
Or so I thought...
While this was very simple, there were a few gotchas.
the tests would run even if only the
readme.md
file was changed.the tests would run even if I wanted to add a new problem, which contained empty boilerplate
Solution.java
file.
How I tackled them
To tackle the first problem, I wrote a shell script to loop through the changed files and if the filename starts with
javaProblems/
which is the root directory for all the problems folders and if the filename ends with.java
only then should it proceed further.Then to get which problem's solution was edited, I used the
awk
command which I learned in the CLI & shell-scripting class at my school.Here's an example of how the
file
variable looked like -javaProblems/problem1/Solution.java
I had to extract the
problem1
identifier, I used-F/
inawk
to divide the file name by/
Then I returned the second field from the result which would beproblemX
.then I
cd
intojavaProblems/problemX
then ran the tests inside that folder andcd
'd out.for file in ${{ steps.changed-files.outputs.all_changed_files }}; do if [[ "$file" == javaProblems/* && "$file" == *.java ]]; then folderName=$(awk -F/ '{print $2}' <<< "$file") echo "Running Tests for $folderName" cd "javaProblems/$folderName" javac Test.java java Test cd ../.. fi done
To tackle the second problem, I added an
if
condition in the workflow fileOnce the PR is labeled with
java-problem-solution
only then should it run the tests.on: pull_request: types: [labeled] paths: - "javaProblems/**" jobs: test-java-solutions: if: ${{ github.event.label.name == 'java-problem-solution' }}
But how were the Test files implemented without any testing libraries?
Let's take an example:
Suppose this is the readme.md
file for the problem
# Max and Min of an Array
You are given an array `A` of type `int[]`.
Return an `array` such that:
- `array[0]` is the minimum of the array `A`
- `array[1]` is the maximum of the array `A`
```
# Example input
A = [1, 2, 3, 4, 5, 6, 7]
# Example output
[1, 7]
```
Here's how the boilerplate Solution.java
file looks like -
// Solution.java
public class Solution {
public int[] solve(int[] A) {
return new int[] {};
}
}
To Test the solution inside the Test.java
file I would instantiate a new Solution
object, pass in the input inside the solve
method and compare the returned answer with the actual answer.
If they are not the same, then it would throw an error.
// Test.java
import java.util.logging.Level;
import java.util.logging.Logger;
public class Test {
public static void main(String[] args) throws Exception {
Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
Solution solveSolution = new Solution();
int[] ans = solveSolution.solve(new int[] { 2, 3, 4, 1, 1, 7, 8 });
int min, max;
try {
min = ans[0];
max = ans[1];
} catch (Exception e) {
logger.log(Level.SEVERE, "Runtime error");
System.exit(1);
throw new Error("Runtime Error");
}
if (min != 1 || max != 8) {
logger.log(Level.SEVERE, "Wrong solution!");
System.exit(1);
throw new Exception("Wrong solution!");
}
System.out.print("Testcase passed!");
}
}
And tada ๐, then I had a working solution that fulfilled every single desired behavior I had thought of.