net.freeutils.util.Tests Maven / Gradle / Ivy
Show all versions of jelementary Show documentation
/*
* Copyright © 2003-2024 Amichai Rothman
*
* This file is part of JElementary - the Java Elementary Utilities package.
*
* JElementary is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* JElementary is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JElementary. If not, see .
*
* For additional info see https://www.freeutils.net/source/jelementary/
*/
package net.freeutils.util;
import java.security.Permission;
import java.util.Arrays;
/**
* The {@code Tests} class facilitates easier and more concise unit tests.
*/
public class Tests {
/**
* The {@code Testable} interface is a callback used to run a single test's processing.
*
* @param the parameter type
* @param the result type
*/
public interface Testable {
/**
* Runs a test using the given parameter as input, and returning the test output.
*
* @param param the test parameter; can be an array if multiple parameters are needed
* @return the test result
* @throws Throwable when running the test with the given input throws any Throwable
*/
R run(P param) throws Throwable;
}
/**
* Asserts that an actual result is equal to the expected result.
* Arrays are deeply-compared.
*
* @param message the exception message to use if the assertion fails
* @param expected the expected result
* @param actual the actual result
* @throws AssertionError if the actual result is not equal to the expected result
*/
private static void assertEquals(String message, Object expected, Object actual) {
// we'd use junit assertions here, but we don't want to have any dependencies
if (expected == actual || expected != null && expected.equals(actual))
return;
Object[] expectedArray = { expected };
Object[] resultArray = { actual };
if (Arrays.deepEquals(expectedArray, resultArray))
return;
throw new AssertionError(message
+ ": expected " + Arrays.deepToString(expectedArray)
+ ", got " + Arrays.deepToString(resultArray));
}
/**
* Runs one or more tests.
*
* Each test data consists of an array of objects where the
* first element is a test description message (type String), the second is the
* parameter(s) to pass to the testable (type P) and the third is the expected
* result (type R). For tests which require multiple parameters, P can be an
* array holding all required parameters.
*
* Alternatively, the test data may consist of an array of only two objects,
* the parameter(s) and the result, and the message is taken to be the string
* representation of the parameter.
*
* For each test, the test data is passed to the testable, and the result is
* compared to the expected result for equality. If the expected result is
* a Class object representing a Throwable, then it is expected that running
* the test will throw an exception of that type.
*
* In both cases, if the expected behavior occurs (same result or exception),
* the test passes. If not, an appropriate assertion error is thrown with the
* corresponding message.
*
* @param
the parameter type
* @param the result type
* @param testable the test operation
* @param tests the test data to operate on
* @throws AssertionError if any of the tests fail
*/
@SuppressWarnings("unchecked")
public static void test(Tests.Testable
testable, Object[]... tests) {
for (Object[] test : tests) {
P param = (P)test[test.length - 2];
R expected = (R)test[test.length - 1];
String message = test.length > 2 ? (String)test[0] : "param '" + param + "'";
boolean fail = expected instanceof Class && Throwable.class.isAssignableFrom((Class>)expected);
try {
if (message == null)
message = String.valueOf(param);
R result = testable.run(param);
if (fail)
throw new AssertionError(message + " should throw " + ((Class>)expected).getName());
assertEquals(message, expected, result);
} catch (Throwable t) {
if (t instanceof AssertionError)
throw (AssertionError)t;
if (!fail || !((Class>)expected).isInstance(t))
throw new AssertionError(message + ": unexpected exception " + t, t);
}
}
}
/**
* Runs one or more tests.
*
* Each test data consists of an array of objects where the
* first element is a test description message (type String) and the second is
* the parameter(s) to pass to the testable (type P). For tests which require
* multiple parameters, P can be an array holding all required parameters.
*
* Alternatively, the test data may consist of an array of only a single object,
* the parameter(s), and the message is taken to be the string representation
* of the parameter.
*
* For each test, the test data is passed to the both testables. If the
* testable's behavior matches the expected testable's behavior (same result or
* exception), the test passes. If not, an appropriate assertion error is thrown
* with the corresponding message.
*
* @param
the parameter type
* @param the result type
* @param testable the test operation
* @param expectedTestable the test operation providing the expected behavior
* @param tests the test data to operate on
*/
@SuppressWarnings("unchecked")
public static void test(Tests.Testable
testable, Tests.Testable
expectedTestable, Object[]... tests) {
for (Object[] test : tests) {
if (test.length > 2)
throw new IllegalArgumentException("test using expectedTestable cannot specify explicit result");
P param = (P)test[test.length - 1];
try {
R expected = expectedTestable.run(param);
test = Containers.concat(test, expected);
} catch (Throwable t) {
test = Containers.concat(test, t.getClass());
}
test(testable, test);
}
}
/**
* The {@code SystemExitException} is thrown when System.exit() is called
* after {@link #preventSystemExit} has been called.
*/
public static class SystemExitException extends SecurityException {
private static final long serialVersionUID = -1273329516299366346L;
private final int status;
public SystemExitException(int status) {
super("System.exit() called with status " + status);
this.status = status;
}
/**
* Returns the status (exit code) that was passed to System.exit().
*
* @return the status (exit code) that was passed to System.exit()
*/
public int getStatus() {
return status;
}
}
/**
* The {@code NoSystemExitSecurityManager} prevents calls to System.exit(),
* throwing a {@link SystemExitException} instead
*/
public static class NoSystemExitSecurityManager extends SecurityManager {
private final SecurityManager prev;
public NoSystemExitSecurityManager(final SecurityManager manager) {
prev = manager;
}
@Override
public void checkExit(int status) {
if (prev != null)
prev.checkExit(status);
throw new SystemExitException(status);
}
@Override
public void checkPermission(Permission perm) {
if (prev != null)
prev.checkPermission(perm);
}
}
/**
* Installs or removes a SecurityManager that prevents
* {@link System#exit} calls from working by throwing an
* {@link SystemExitException} instead.
*
* @param noExit if true, exit calls are prevented;
* if false, exit calls are allowed
*/
public static void preventSystemExit(boolean noExit) {
SecurityManager manager = System.getSecurityManager();
if (manager instanceof NoSystemExitSecurityManager) {
if (!noExit)
System.setSecurityManager(((NoSystemExitSecurityManager)manager).prev);
} else {
if (noExit)
System.setSecurityManager(new NoSystemExitSecurityManager(manager));
}
}
}