Test unitarios con JBang
Llevo estos días preparando la charla que voy a dar en la commit-conf este año y me he encontrado con una cosa que no conocía y que me ha parecido bastante interesante: es posible ejecutar test unitarios JUnit con JBang.
Si no conocéis JBang os recomiendo que le echéis un vistazo. Este es un ejemplo sacado de la documentación oficial.
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 25+
//DEPS com.github.lalyos:jfiglet:0.0.8
import com.github.lalyos.jfiglet.FigletFont;
void main() throws Exception {
IO.println(FigletFont.convertOneLine("Hello JBang!"));
}
Si instaláis jbang, copiáis este código en un fichero llamado hello.java, luego lo podéis
ejecutar con el comando jbang hello.java. Y esto es lo que sacaría por consola:
_ _ _ _ _ ____ _
| | | | ___ | | | | ___ | | | __ ) __ _ _ __ __ _ | |
| |_| | / _ \ | | | | / _ \ _ | | | _ \ / _` | | '_ \ / _` | | |
| _ | | __/ | | | | | (_) | | |_| | | |_) | | (_| | | | | | | (_| | |_|
|_| |_| \___| |_| |_| \___/ \___/ |____/ \__,_| |_| |_| \__, | (_)
|___/
Si le dais permiso de ejecución al fichero chmod +x hello.java, se puede ejecutar
directamente como si fuera cualquier otro comando ./hello.java.
Todo está muy chulo, pero se pueden hacer cosas todavía más chulas. Como por ejemplo ejecutar tests unitarios.
//DEPS org.assertj:assertj-core:3.27.7
//DEPS org.junit.jupiter:junit-jupiter:6.0.3
//JAVA 25+
//SOURCES Program.java,Either.java
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
public class ProgramTest {
@Test
void shouldCalculateFibSequenceRecursive() {
var program = fibMemoized.apply(20);
var result = program.eval(null);
assertEquals(Either.right(10946), result);
}
@Test
void shouldCalculateFibSequenceNonRecursive() {
var result = fib(20);
assertEquals(10946, result);
}
static Function<Integer, Program<Void, Void, Integer>> fibMemoized = Program.memoize(
new Function<Integer, Program<Void, Void, Integer>>() {
@Override
public Program<Void, Void, Integer> apply(Integer n) {
if (n < 2) {
return Program.success(1);
}
var fib2 = Program.suspend(() -> fibMemoized.apply(n - 2));
var fib1 = Program.suspend(() -> fibMemoized.apply(n - 1));
return Program.zip(fib2, fib1, Integer::sum);
}
});
static int fib(int n) {
if (n < 2) {
return 1;
}
int a = 1, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}
}
Aquí tenemos un test unitario creado para la librería en la que hemos estado trabajando en mis últimos artículos.
Añadimos con la directiva //DEP las dependencias a junit y assertj usando las coordenadas
de maven, de una manera muy similar a como hacemos en gradle u otro gestor de paquetes.
Con //SOURCES podemos añadir otros fichero java para que se compilen junto a la clase de
test.
La directiva //JAVA lo que hace jbang es ver si tienes instalado una jdk compatible
con la versión que necesitas. Si usas sdkman, jbang buscará entre todas las versiones
de java que tengas una compatible.
Ahora bien, ¿cómo los ejecutamos?
Primero tenemos que compilar los tests:
jbang build ProgramTest.java
Esto generará un fichero jar con esta estructura:
0 Mon May 11 23:33:58 CEST 2026 META-INF/
79 Mon May 11 23:33:58 CEST 2026 META-INF/MANIFEST.MF
1653 Mon May 11 23:33:58 CEST 2026 Either$Left.class
1661 Mon May 11 23:33:58 CEST 2026 Either$Right.class
2329 Mon May 11 23:33:58 CEST 2026 Either.class
0 Mon May 11 23:33:58 CEST 2026 META-INF/maven/
0 Mon May 11 23:33:58 CEST 2026 META-INF/maven/group/
2302 Mon May 11 23:33:58 CEST 2026 META-INF/maven/group/pom.xml
1911 Mon May 11 23:33:58 CEST 2026 Program$Access.class
1710 Mon May 11 23:33:58 CEST 2026 Program$Failure.class
2229 Mon May 11 23:33:58 CEST 2026 Program$FlatMap.class
2264 Mon May 11 23:33:58 CEST 2026 Program$FlatMapError.class
1764 Mon May 11 23:33:58 CEST 2026 Program$Memoized.class
1710 Mon May 11 23:33:58 CEST 2026 Program$Success.class
12389 Mon May 11 23:33:58 CEST 2026 Program.class
2511 Mon May 11 23:33:58 CEST 2026 ProgramTest$1.class
1923 Mon May 11 23:33:58 CEST 2026 ProgramTest.class
Ahora ya podemos ejecutar el test, lo podemos hacer de esta forma:
jbang junit@junit-team execute --class-path `jbang info classpath ProgramTest.java` --scan-classpath
El resultado será el siguiente:
Thanks for using JUnit! Support its development at https://junit.org/sponsoring
╷
├─ JUnit Platform Suite ✔
├─ JUnit Jupiter ✔
│ └─ ProgramTest ✔
│ ├─ shouldCalculateFibSequenceRecursive() ✔
│ └─ shouldCalculateFibSequenceNonRecursive() ✔
└─ JUnit Vintage ✔
Test run finished after 112 ms
[ 4 containers found ]
[ 0 containers skipped ]
[ 4 containers started ]
[ 0 containers aborted ]
[ 4 containers successful ]
[ 0 containers failed ]
[ 2 tests found ]
[ 0 tests skipped ]
[ 2 tests started ]
[ 0 tests aborted ]
[ 2 tests successful ]
[ 0 tests failed ]
Cuando un test falla este es el resultado:
Thanks for using JUnit! Support its development at https://junit.org/sponsoring
╷
├─ JUnit Platform Suite ✔
├─ JUnit Jupiter ✔
│ └─ ProgramTest ✔
│ ├─ shouldCalculateFibSequenceRecursive() ✘ expected: <Right[right=109460]> but was: <Right[right=10946]>
│ └─ shouldCalculateFibSequenceNonRecursive() ✔
└─ JUnit Vintage ✔
Failures (1):
JUnit Jupiter:ProgramTest:shouldCalculateFibSequenceRecursive()
MethodSource [className = 'ProgramTest', methodName = 'shouldCalculateFibSequenceRecursive', methodParameterTypes = '']
=> org.opentest4j.AssertionFailedError: expected: <Right[right=109460]> but was: <Right[right=10946]>
org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:158)
org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:139)
org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:201)
org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:184)
org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:179)
org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1188)
ProgramTest.shouldCalculateFibSequenceRecursive(ProgramTest.java:21)
Test run finished after 140 ms
[ 4 containers found ]
[ 0 containers skipped ]
[ 4 containers started ]
[ 0 containers aborted ]
[ 4 containers successful ]
[ 0 containers failed ]
[ 2 tests found ]
[ 0 tests skipped ]
[ 2 tests started ]
[ 0 tests aborted ]
[ 1 tests successful ]
[ 1 tests failed ]
Y no solo eso podemos ejecutar tests, podemos también ejecutar micro benchmarks del tipo JMH. Por ejemplo:
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.openjdk.jmh:jmh-core:1.37
//DEPS org.openjdk.jmh:jmh-generator-annprocess:1.37
//SOURCES ProgramTest.java,Program.java,Either.java
//JAVA 25+
//JAVAC_OPTIONS -processor org.openjdk.jmh.generators.BenchmarkProcessor
package test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.*;
import org.openjdk.jmh.runner.options.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class Bench {
private String text = "hello";
@Benchmark
public Integer iterative() {
return ProgramTest.fib(20);
}
@Benchmark
public Either<Void, Integer> recursive() {
return ProgramTest.fibMemoized.apply(20).eval(null);
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(Bench.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
Si le damos permiso de ejecución al archivo chmod +x Bench.java, y lo ejecutamos
./Bench.java se ejecutará el benchmark.
Aquí lo único especial es la directiva //JAVAC_OPTIONS, que la necesitamos añadir
ya que JMH genera código a partir de las anotaciones y hay añadir explícitamente
el gestor de anotaciones para genere todo el código necesario.
[jbang] Building jar for Bench.java...
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/vant/.m2/repository/org/openjdk/jmh/jmh-core/1.37/jmh-core-1.37.jar)
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
# JMH version: 1.37
# VM version: JDK 25.0.3, OpenJDK 64-Bit Server VM, 25.0.3+9-LTS
# VM invoker: /home/vant/.sdkman/candidates/java/25.0.3-tem/bin/java
# VM options: <none>
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: test.Bench.iterative
# Run progress: 0,00% complete, ETA 00:03:20
# Fork: 1 of 1
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/vant/.m2/repository/org/openjdk/jmh/jmh-core/1.37/jmh-core-1.37.jar)
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
# Warmup Iteration 1: 1,514 ns/op
# Warmup Iteration 2: 1,743 ns/op
# Warmup Iteration 3: 1,754 ns/op
# Warmup Iteration 4: 1,671 ns/op
# Warmup Iteration 5: 1,859 ns/op
Iteration 1: 1,750 ns/op
Iteration 2: 1,815 ns/op
Iteration 3: 1,708 ns/op
Iteration 4: 1,725 ns/op
Iteration 5: 1,657 ns/op
Result "test.Bench.iterative":
1,731 ±(99.9%) 0,224 ns/op [Average]
(min, avg, max) = (1,657, 1,731, 1,815), stdev = 0,058
CI (99.9%): [1,507, 1,955] (assumes normal distribution)
# JMH version: 1.37
# VM version: JDK 25.0.3, OpenJDK 64-Bit Server VM, 25.0.3+9-LTS
# VM invoker: /home/vant/.sdkman/candidates/java/25.0.3-tem/bin/java
# VM options: <none>
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: test.Bench.recursive
# Run progress: 50,00% complete, ETA 00:01:40
# Fork: 1 of 1
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by org.openjdk.jmh.util.Utils (file:/home/vant/.m2/repository/org/openjdk/jmh/jmh-core/1.37/jmh-core-1.37.jar)
WARNING: Please consider reporting this to the maintainers of class org.openjdk.jmh.util.Utils
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
# Warmup Iteration 1: 2282,098 ns/op
# Warmup Iteration 2: 2462,642 ns/op
# Warmup Iteration 3: 2353,671 ns/op
# Warmup Iteration 4: 2388,340 ns/op
# Warmup Iteration 5: 2407,009 ns/op
Iteration 1: 2411,544 ns/op
Iteration 2: 2308,276 ns/op
Iteration 3: 2444,991 ns/op
Iteration 4: 2417,737 ns/op
Iteration 5: 2324,341 ns/op
Result "test.Bench.recursive":
2381,378 ±(99.9%) 234,824 ns/op [Average]
(min, avg, max) = (2308,276, 2381,378, 2444,991), stdev = 60,983
CI (99.9%): [2146,554, 2616,202] (assumes normal distribution)
# Run complete. Total time: 00:03:20
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
extra caution when trusting the results, look into the generated code to check the benchmark still
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
different JVMs are already problematic, the performance difference caused by different Blackhole
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.
Benchmark Mode Cnt Score Error Units
Bench.iterative avgt 5 1,731 ± 0,224 ns/op
Bench.recursive avgt 5 2381,378 ± 234,824 ns/op
Como hemos podido ver, jbang es una herramienta estupenda. Puedes usarlo como un gestor de paquetes, para crear scripts en Java, ejecutar tests unitarios y micro benchmarks.