How I implemented Java Code Testing without any build automation tool for my school's club

How I implemented Java Code Testing without any build automation tool for my school's club

ยท

4 min read

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/ in awk to divide the file name by /Then I returned the second field from the result which would be problemX .

    • then I cd into javaProblems/problemX then ran the tests inside that folder and cd'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 file

    • Once 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.

ย