Testing Java Lox with jUnit
If you are going through a book like Crafting Interpreters and just input the code from the book and run it, maybe you don’t need tests. I would also argue you are probably not going to learn the material very well. Writing tests on its own will help build your understanding of the code the book is explaining to you, but the real point of adding unit tests is to support doing the exercises. I am not going to bother explaining the value of unit tests in general or test driven development, but I think it’s worth highlighting that the value of unit tests is particularly clear when you are refactoring and extending an existing codebase (which is basically what you are doing when you implement the features from the exercises).
This is a quick guide to the features from jUnit I used to test jLox. I used the bare minimum number of concepts I felt that I needed to write tests effectively, so this is not a comprehensive guide to jUnit, and not necessarily following best practices. I wrote most of my tests as integration tests once the full interpreter stack was available: feeding Lox code to the interpreter and using print statements to test the output. If I was implementing a real interpreter I would do a much more careful job testing the individual components with unit tests, but those integration tests were good enough to help debug issues that I inevitably introduced.
Test Cases
Test cases are placed in a test directory that sits next to main. Inside it has a directory structure that mirrors main. Tests should be placed in directories mirroring the files they test.
e.g. app/src/test/java/com/craftinginterpreters/lox/InterpreterTest.java
package com.craftinginterpreters.lox;
import org.junit.Test;
public class InterpreterTest {
@Test public void testStuff() {}
}
Assertions
jUnit comes with a bunch of different types of assertions. I only used a few. They are all pretty self explanatory. Check out the docs for a list of assertion methods available to you: https://junit.org/junit4/javadoc/4.8/org/junit/Assert.html
app/src/test/java/com/craftinginterpreters/lox/InterpreterTest.java
package com.craftinginterpreters.lox;
import org.junit.Test;
import static org.junit.Assert.*;
public class InterpreterTest {
@Test public void testAsserts() {
assertEquals("true equals true", true, true);
assertTrue("true is true", true);
assertNotNull("true is not null", true);
}
}
Testing Exceptions
I used one more type of assertion that I thought deserves a little
attention. assertThrows
allows you to test methods that throw
exceptions by passing it a lambda.
app/src/test/java/com/craftinginterpreters/lox/InterpreterTest.java
package com.craftinginterpreters.lox;
import org.junit.Test;
import static org.junit.Assert.*;
import java.lang.UnsupportedOperationException;
public class InterpreterTest {
@Test public void testException() {
RuntimeError exception =
assertThrows(UnsupportedOperationException.class, () ->
throw new UnsupportedOperationException("Not implemented"));
assertEquals("Not implemented", exception.getMessage());
}
}
Print output
Once you have the interpreter was set up, I found that the easiest way to test output from the system was the call the built in Lox function print, and then check that the correct strings were sent to standard output. It turns out it’s pretty easy to capture the standard output and test it with jUnit.
app/src/test/java/com/craftinginterpreters/lox/InterpreterTest.java
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.List;
import org.junit.Test;
import org.junit.Before;
public class InterpreterTest {
private final PrintStream standardOut = System.out;
private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
@Before
public void setUp() {
System.setOut(new PrintStream(outputStreamCaptor));
}
@Test public void testPrint() {
System.out.println("lol");
assertEquals("lol", outputStreamCaptor.toString().trim());
}
}
I got most of this information from here if you are curious: https://www.baeldung.com/java-testing-system-out-println
Focusing a Test case
If you only want to run a single test case (especially useful if you are doing print debugging and drowning in output!), you can easily do that by passing a command line argument. If you are not using gradle, I assume it’s pretty similar:
./gradlew test --tests InterpreterTest.testPrint
Setting up a Mock Clock
At some point Crafting Interpreters will have you implement a clock
function that grabs the system time and converts in to seconds. As I
struggled to find a a way to override and mock out
System.currentTimeMillis()
, I realized that instead I could just set
up the interpreter to accept a clock object as a constructor argument
(defaulting to the system clock if none is provided), so I could use a
mock clock in the tests. This is kinda overkill, but if you wanted to
have this in your life here are the clock classes I used to set that
up. Just create an instance of whichever clock and call
clock.getCurrentTimeSeconds()
instead of calling
System.currentTimeMillis()
directly inside the interpreter
code. Inside your tests pass a TestClock
to your interpreter, and then
you can control the time with tick()
.
app/src/main/java/com/craftinginterpreters/lox/LoxClock.java
package com.craftinginterpreters.lox;
import java.util.List;
interface LoxClock {
double getCurrentTimeSeconds();
}
app/src/main/java/com/craftinginterpreters/lox/SystemClock.java
package com.craftinginterpreters.lox;
import java.util.List;
class SystemClock implements LoxClock {
public double getCurrentTimeSeconds() {
return (double) System.currentTimeMillis() / 1000.0;
}
}
app/src/main/java/com/craftinginterpreters/lox/TestClock.java
package com.craftinginterpreters.lox;
import java.util.List;
class TestClock implements LoxClock {
int currentTime = 0;
public double getCurrentTimeSeconds() {
return (double)currentTime;
}
public void tick() {
tick(1);
}
public void tick(int seconds) {
currentTime += seconds;
}
}