|
import java.util.HashMap; |
|
|
|
/** |
|
* We wrote TestAssertions as a helper class to make writing tests in Java |
|
* faster, easier, more concise, and less error prone. |
|
* <p> |
|
* The code within TestAssertions is not very readable and we may not have |
|
* cought all possible bugs yet. |
|
* Our reasoning was that while core code (such as CentreIO) should be simple |
|
* and bug free, helper code purely for tests can doesn't have to be so |
|
* strictly coded. This is becaus if the test code breaks the rest of the |
|
* program will be unaffected. You would never run tests in a production |
|
* environment on production data, you would always use test data just in |
|
* case there is a data loss bug in test code. Thus, wwe know the code in |
|
* TestAssertions is complex, but we think in this case it is acceptable |
|
* and justified because it makes test code written using |
|
* TestAssertions so much clearer and quicker. |
|
* <p> |
|
* We have put inline coments to try and explain some of the more obscure |
|
* complex bits of Java we have used but it might still not be very |
|
* understandable. Again, as this isn't a core class we expect it to be used |
|
* by 'client' classes and thus the programmer does not need to understand |
|
* how TestAssertions works internally, just how to use it. |
|
* <p> |
|
* Usage:<br> |
|
* <br> |
|
* You must call always method() first.<br> |
|
* You may optionally call a single should_*() method afterwards.<br> |
|
* You may optionally call reason() at any time.<br> |
|
* You may optionally call results() after calling a should_*() method.<br> |
|
* You may optionally call output() to print the output of the method call.<br> |
|
* You may optionally call stats() once after running all tests to print test statistics CANNOT BE CHAINED.<br> |
|
* You may optionally call return_output() last to return the output of the method call CANNOT BE CHAINED.<br> |
|
* <p> |
|
* To run tests on an object called mathExamples create a new TestAssertions |
|
* object and pass the mathExamples object into the constructor: |
|
* <br><code> |
|
* TestAssertions testAssertions = new TestAssertions(mathExamples); |
|
* </code><br> |
|
* Then to test the method fibonacci(int n) with actual parameter 5 to check if |
|
* the result equals the String "0,1,1,2,3" and print a reason, you would write: |
|
* <code><br> |
|
* testAssertions.method("fibonacci", 5).should_equal("0,1,1,2,3").reason("First 5 elements of the fibonacci sequence").test() |
|
* <br></code> |
|
* <br> |
|
* See Tester class for more exampe usage. |
|
* |
|
* @author Leddy, K (10493062); George, S (14092969) |
|
* @version 1.0.0 |
|
*/ |
|
@SuppressWarnings({"unchecked", "serial"}) //We do our own checking so warning is unnecesary |
|
public class TestAssertion { |
|
private Object obj; |
|
private Object output; |
|
private String resultString; |
|
private String invokeException; |
|
private int passes; |
|
private int failures; |
|
private int totalTests; |
|
private static HashMap<Class<?>, Class<?>> PRIMITIVES_TO_WRAPPERS = |
|
new HashMap<Class<?>, Class<?>>() {{ |
|
put(boolean.class, Boolean.class); |
|
put(byte.class, Byte.class); |
|
put(char.class, Character.class); |
|
put(double.class, Double.class); |
|
put(float.class, Float.class); |
|
put(int.class, Integer.class); |
|
put(long.class, Long.class); |
|
put(short.class, Short.class); |
|
put(void.class, Void.class); |
|
}};; |
|
|
|
/** |
|
* Create a TestAssertions object with reference to the passed in object to run test on. |
|
* |
|
* @param obj the object to run tests on. |
|
*/ |
|
public TestAssertion(Object obj) { |
|
//This is the object we will call methods on |
|
this.obj = obj; |
|
|
|
//Initialise test stats |
|
passes = 0; |
|
failures = 0; |
|
totalTests = 0; |
|
} |
|
|
|
/** |
|
* Call the method specified as a String with the parameters passed. |
|
* |
|
* @param methodName The name of the method to be called on obj. |
|
* @param params the (optional) params to be passed to the method to be called. |
|
*/ |
|
public TestAssertion method(String methodName, Object... params) { |
|
totalTests += 1; |
|
|
|
//build up a string representation of the parameters |
|
String paramsString = ""; |
|
for (int i = 0; i < params.length; i += 1) { |
|
paramsString += formatObjectForPrinting(params[i]); |
|
//append a comma to the end of every parameter except the last |
|
if (i != params.length-1) paramsString += ", "; |
|
} |
|
|
|
System.out.print("\n" + totalTests + ". " + methodName + "(" + paramsString + ")"); //print name of method we are testing |
|
|
|
//Reset state |
|
output = null; |
|
resultString = "(TestAssertion Notice) You didn't call a should_*() method so no expected result to test."; |
|
invokeException = null; |
|
|
|
try { |
|
output = DynamicMethodCall.run(obj, methodName, params); |
|
} catch (Exception e) { |
|
//Remaining exception must have come from method itself, store it |
|
invokeException = e.toString(); |
|
} |
|
|
|
return this; //Always return 'this' to enable chaining |
|
} |
|
|
|
private String formatObjectForPrinting(Object unformatted) { |
|
//for primitives, call the default toString() method |
|
if (unformatted == null) |
|
return null; |
|
else if (PRIMITIVES_TO_WRAPPERS.containsValue(unformatted.getClass())) |
|
return unformatted.toString(); |
|
//for String objects, surround the string with quotes |
|
else if (unformatted instanceof String) |
|
return "\"" + unformatted + "\""; |
|
//for any object type, use name of class surrounded by square brackets |
|
else |
|
return "[" + unformatted.getClass().getName() + "]"; |
|
} |
|
|
|
/** |
|
* Test that the result of calling the method was true. |
|
* |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_be_true() { |
|
System.out.print(" should be true."); |
|
should_boolean(true, true); |
|
//should_boolean(false, false); //Equivalent to above |
|
return this; |
|
} |
|
|
|
/** |
|
* Test that the result of calling the method was false. |
|
* |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_be_false() { |
|
System.out.print(" should be false."); |
|
should_boolean(false, true); |
|
//should_boolean(true, false); //Equivalent to above |
|
return this; |
|
} |
|
|
|
/** |
|
* Test that the result of calling the method was equal to |
|
* the passed parameter. |
|
* |
|
* @param obj the data to test if it was the same as the return |
|
* result of calling method. |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_equal(Object obj) { |
|
System.out.print(" should equal " + formatObjectForPrinting(obj) + "."); |
|
should_boolean(obj, true); |
|
return this; |
|
} |
|
|
|
/** |
|
* Test that the result of calling the method was not equal to |
|
* the passed parameter. |
|
* |
|
* @param obj the data to test if it was not the same as the return |
|
* result of calling method. |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_not_equal(Object obj) { |
|
System.out.print(" should not equal " + formatObjectForPrinting(obj) + "."); |
|
should_boolean(obj, false); |
|
return this; |
|
} |
|
|
|
/** |
|
* Test that the result of calling the method contains the passed String. |
|
* |
|
* @param obj the string to test if the return result of |
|
* calling method contained it. |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_contain(String string) { |
|
System.out.print(" should contain " + formatObjectForPrinting(string) + "."); |
|
should_contain_boolean(string, true); |
|
return this; |
|
} |
|
|
|
/** |
|
* Test that the result of calling the method does not |
|
* contain the passed String. |
|
* |
|
* @param obj the string to test if the return result of |
|
* calling method didn't contain it. |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_not_contain(String string) { |
|
System.out.print(" should contain " + formatObjectForPrinting(string) + "."); |
|
should_contain_boolean(string, false); |
|
return this; |
|
} |
|
|
|
private TestAssertion should_contain_boolean(String string, boolean shouldContain) { |
|
boolean success = false; |
|
|
|
if (output instanceof String) |
|
success = ((String)output).toLowerCase().contains(string.toLowerCase()) == shouldContain; |
|
else |
|
System.out.println("should_contain() cannot be called on methods that don't return a String"); |
|
|
|
resultString = success ? null : ("method returned: " + formatObjectForPrinting(output) + "."); |
|
return this; |
|
} |
|
|
|
private void should_boolean(Object obj, boolean shouldEqual) { |
|
boolean success; |
|
if (obj instanceof String |
|
|| PRIMITIVES_TO_WRAPPERS.containsValue(obj.getClass())) |
|
success = obj.equals(output) == shouldEqual; |
|
else |
|
success = (obj == output) == shouldEqual; |
|
|
|
resultString = success ? null : ("method returned: " + formatObjectForPrinting(output) + "."); |
|
} |
|
|
|
/** |
|
* Test that the result of calling the method was null. |
|
* |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_be_null() { |
|
System.out.print(" should be null."); |
|
|
|
resultString = (output == null) ? |
|
null : |
|
("method returned: " + formatObjectForPrinting(output) + "."); |
|
|
|
return this; |
|
} |
|
|
|
/** |
|
* Test that the result of calling the method was not null. |
|
* |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_not_be_null() { |
|
System.out.print(" should not be null."); |
|
|
|
resultString = (output != null) ? |
|
null : |
|
("method returned: " + formatObjectForPrinting(output) + "."); |
|
|
|
return this; |
|
} |
|
|
|
/** |
|
* Test that calling the method did not raise an exception. |
|
* |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_not_raise_exception() { |
|
System.out.print(" should not raise exception."); |
|
|
|
resultString = (invokeException == null) ? |
|
null : |
|
("method raised: [" + invokeException + "]"); |
|
|
|
return this; |
|
} |
|
|
|
/** |
|
* Test that calling the method raised an exception. |
|
* |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion should_raise_exception() { |
|
System.out.print(" should raise exception."); |
|
|
|
resultString = (invokeException != null) ? |
|
null : |
|
("method did not raise exception."); |
|
|
|
return this; |
|
} |
|
|
|
/** |
|
* Print the reason for performing the test. |
|
* |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion reason(String reason) { |
|
System.out.println("\nReason: " + reason); |
|
return this; |
|
} |
|
|
|
/** |
|
* Print the return result of calling the method (useful for debugging). |
|
* |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion output() { |
|
//Print method output |
|
System.out.println("\nMethod Output: \n" + output); |
|
return this; |
|
} |
|
|
|
/** |
|
* Print the results of running the should_*() test. |
|
* |
|
* @return this object to enable method chaining. |
|
*/ |
|
public TestAssertion result() { |
|
//Print test result |
|
if (resultString == null) { |
|
System.out.println("Success: 'should' condition met."); |
|
passes += 1; |
|
} |
|
else { |
|
System.out.println("Failed: " + resultString); |
|
failures += 1; |
|
} |
|
return this; |
|
} |
|
|
|
/** |
|
* Return the return result of calling the method. |
|
* |
|
* @return the return result of calling the method. |
|
*/ |
|
public Object return_output() { |
|
return output; |
|
} |
|
|
|
/** |
|
* Print stats about the number of tests run. |
|
*/ |
|
public void stats() { |
|
//Not all calls to method() are followed by should_*() and result() |
|
//so totalTests may be > passes + failures. |
|
System.out.println("\n\nTests run: " + totalTests + ", Test Passes: " + passes + ", Test Failures: " + failures); |
|
} |
|
} |