|
|
|
Advertisement
|
This page shows the complete listing of class com.artima.examples.scriptdriven.ex1.ScriptDrivenAccountSuite, which is
described in the article, "Drive Your Unit Tests with Custom Scripts":
http://www.artima.com/suiterunner/scriptdriven.html
/*
* Copyright (C) 2001-2003 Artima Software, Inc. All rights reserved.
* Licensed under the Open Software License version 1.0.
*
* A copy of the Open Software License version 1.0 is available at:
* http://www.artima.com/suiterunner/osl10.html
*
* This software consists of voluntary contributions made by many
* individuals on behalf of Artima Software, Inc. For more
* information on Artima Software, Inc., please see:
* http://www.artima.com/
*/
package com.artima.examples.scriptdriven.ex1;
import com.artima.examples.account.ex6.Account;
import com.artima.examples.account.ex6.InsufficientFundsException;
import org.suiterunner.Suite;
import org.suiterunner.Reporter;
import org.suiterunner.Report;
import java.io.*;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
/**
* A <code>Suite</code> of tests, defined in a script file, for the
* <code>com.artima.examples.account.ex6.Account</code> class.
* The name of the script file is taken from the <code>ScriptFile</code>
* Java property. If <code>ScriptFile</code> is not set,
* <code>script.txt</code> is used as the script file.
*
* <p>
* The script file contains a sequence of test commands
* created specifically to test
* <code>com.artima.examples.account.ex6.Account</code>
* (<code>Account</code>). Each line can contain at most one
* test command, and each test command must appear in full on
* a single line. A line can contain no command, allowing for
* blank lines that can help visually separate the various
* logically sections of the test script.
* A pound sign (#), and any characters appearing after it to the
* end of the line are ignored, allowing for comments.
*
* <p>
* The four test commands are:
* <ul>
* <li><code>newAccount</code>
* <li><code>getBalance</code>
* <li><code>deposit</code>
* <li><code>withdraw</code>
* </ul>
*
* <p>
* The <code>newAccount</code> commands causes
* <code>ScriptDrivenAccountSuite</code> to create a new
* <code>Account</code> instance on which all subsequent commands
* (until the next <code>newAccount</code>) will operate. Here's
* an example:
*
* <pre>
* newAccount
* </pre>
*
* <p>
* The <code>getBalance</code> command takes one argument, a long
* expected return value. This command will cause
* <code>ScriptDrivenAccountSuite</code> to invoke <code>getBalance</code> on
* the <code>Account</code> instance (created by the most recent
* <code>newAccount</code> command), and compare the return value with the
* expected value specified as the first argument to the <code>getBalance</code>
* command. If the values are equal, a <code>testSucceeded</code>
* is invoked on the <code>Reporter</code>. Else, <code>testFailed</code>
* is invoked on the <code>Reporter</code>. Here's an example:
*
* <pre>
* getBalance 100
* </pre>
*
* <p>
* The <code>deposit</code> command has two forms. The first form
* takes one argument, a long amount to deposit. This command will
* cause <code>ScriptDrivenAccountSuite</code> to invoke
* <code>deposit</code> on the <code>Account</code> instance,
* passing in the specified amount to deposit. If <code>deposit</code>
* throws an exception, <code>testFailed</code> will be invoked on
* the <code>Reporter</code>. Otherwise, <code>testSucceeded</code>
* will be invoked. Here's an example:
*
* <pre>
* deposit 10
* </pre>
*
* <p>
* The second form of the <code>deposit</code> command has two arguments,
* a long amount to deposit and the fully qualified name of an expected
* exception. This command will cause <code>ScriptDrivenAccountSuite</code>
* to invoke <code>deposit</code> on the <code>Account</code> instance,
* passing in the specified amount to deposit. If <code>deposit</code>
* throws exactly the exception specified in the second argument,
* <code>testSucceeded</code> will be invoked on the <code>Reporter</code>.
* Otherwise, <code>testFailed</code> will be invoked. Here's an example:
*
* <pre>
* deposit 1000 java.lang.ArithmeticException
* </pre>
*
* <p>
* The <code>withdraw</code> command has two forms. The first form
* takes one argument, a long amount to withdraw. This command will
* cause <code>ScriptDrivenAccountSuite</code> to invoke
* <code>withdraw</code> on the <code>Account</code> instance,
* passing in the specified amount to withdraw. If <code>withdraw</code>
* throws an exception, <code>testFailed</code> will be invoked on
* the <code>Reporter</code>. Otherwise, <code>testSucceeded</code>
* will be invoked. Here's an example:
*
* <pre>
* withdraw 20
* </pre>
*
* <p>
* The second form of the <code>withdraw</code> command has two arguments,
* a long amount to withdraw and the fully qualified name of an expected
* exception. This command will cause <code>ScriptDrivenAccountSuite</code>
* to invoke <code>withdraw</code> on the <code>Account</code> instance,
* passing in the specified amount to withdraw. If <code>withdraw</code>
* throws exactly the exception specified in the second argument,
* <code>testSucceeded</code> will be invoked on the <code>Reporter</code>.
* Otherwise, <code>testFailed</code> will be invoked. Here's an example:
*
* <pre>
* withdraw 10 com.artima.examples.account.ex6.InsufficientFundsException
* </pre>
*/
public class ScriptDrivenAccountSuite extends Suite {
private static String DEFAULT_SCRIPT_FILE = "script.txt";
private static String scriptFileName;
private Account account = new Account();
/**
* Construct a new <code>ScriptDrivenAccountSuite</code>. If
* the <code>ScriptFile</code> system property is set, the
* value of that property will be used as the script file
* name. Else, <code>script.txt</code> will be used.
*/
public ScriptDrivenAccountSuite() {
scriptFileName = System.getProperty("ScriptFile");
if (scriptFileName == null) {
scriptFileName = DEFAULT_SCRIPT_FILE;
}
}
/**
* Get the total number of tests that are expected to run
* when this suite object's <code>execute</code> method is invoked.
* This class's implementation of this method returns the sum of:
*
* <ul>
* <li>the number of test commands contained in the script file
* <li>the sum of the values obtained by invoking
* <code>getTestCount</code> on every sub-<code>Suite</code>
* contained in this suite object.
* </ul>
*/
public int getTestCount() {
int testCount = 0;
// Each newAccount, deposit, withdraw, and getBalance command
// in the file is an expected test, so open the file and count these.
BufferedReader reader = null;
try {
FileInputStream fis = new FileInputStream(scriptFileName);
InputStreamReader isr = new InputStreamReader(fis);
reader = new BufferedReader(isr);
}
catch (FileNotFoundException e) {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
throw new RuntimeException("Unable to open " + scriptFileName
+ ". " + e.getMessage());
}
try {
String line = reader.readLine();
while (line != null) {
line = trimLine(line);
// Count any non-empty line as a test
if (line.length() != 0) {
++testCount;
}
line = reader.readLine();
}
}
catch (IOException e) {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
throw new RuntimeException("Unable to read a line from script file "
+ scriptFileName + ". " + e.getMessage());
}
finally {
try {
reader.close();
}
catch (IOException e) {
throw new RuntimeException("Unable to close script file " +
scriptFileName + ". " + e.getMessage());
}
}
// Count the tests in each sub-Suite too
Iterator it = getSubSuites().iterator();
while (it.hasNext()) {
Object o = it.next();
Suite subSuite = (Suite) o;
testCount += subSuite.getTestCount();
}
return testCount;
}
/**
* Execute this suite object.
*
* <P>This class's implementation of this method
* executes the test commands contained in the script file,
* then invokes <code>executeSubSuites</code> on itself,
* passing in the specified <code>Reporter<code>.
*
* @param reporter the <code>Reporter</code> to which results will be reported
* @exception NullPointerException if <CODE>reporter</CODE> is <CODE>null</CODE>.
*/
public void execute(Reporter reporter) {
if (reporter == null) {
throw new NullPointerException("reporter is null");
}
BufferedReader reader = null;
try {
FileInputStream fis = new FileInputStream(scriptFileName);
InputStreamReader isr = new InputStreamReader(fis);
reader = new BufferedReader(isr);
}
catch (FileNotFoundException e) {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
throw new RuntimeException("Unable to open " + scriptFileName
+ ". " + e.getMessage());
}
try {
int lineNum = 1;
String line = reader.readLine();
while (line != null) {
if (isStopRequested()) {
return;
}
processLine(reporter, line, lineNum);
line = reader.readLine();
++lineNum;
}
}
catch (IOException e) {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
throw new RuntimeException("Unable to read a line from script file "
+ scriptFileName + ". " + e.getMessage());
}
finally {
try {
reader.close();
}
catch (IOException e) {
throw new RuntimeException("Unable to close script file " +
scriptFileName + ". " + e.getMessage());
}
}
executeSubSuites(reporter);
}
// Trim comments and white space from line. Comments begin with
// a '#' character and extend to the end of the line
private String trimLine(String line) {
// First, remove any comments from the line, then trim the
// whitespace off both ends.
int poundSignPos = line.indexOf('#');
if (poundSignPos >= 0) {
line = line.substring(0, poundSignPos);
}
line = line.trim();
return line;
}
private void processLine(Reporter reporter, String line, int lineNum) {
line = trimLine(line);
// Ignore blank lines
if (line.length() == 0) {
return;
}
List tokensList = getTokens(line, lineNum);
// First token must be a string with the value of "newAccount",
// "getBalance", "deposit", or "withdraw".
Object token = tokensList.get(0);
if (token instanceof String) {
String command = (String) token;
if (command.equals("newAccount")) {
processNewAccount(reporter, lineNum);
}
else if (command.equals("getBalance")) {
processGetBalance(reporter, tokensList.subList(1,
tokensList.size()), lineNum);
}
else if (command.equals("deposit")) {
processDeposit(reporter, tokensList.subList(1,
tokensList.size()), lineNum);
}
else if (command.equals("withdraw")) {
processWithdraw(reporter,
tokensList.subList(1, tokensList.size()), lineNum);
}
else {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("Initial token in line must be one of "
+ "\"newAccount\", \"getBalance\", \"deposit\", or \"withdraw\". "
+ " Problem token: " + token.toString(), lineNum);
throw new RuntimeException(msg);
}
}
else {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("Initial token in line must be one of "
+ "\"newAccount\", \"getBalance\", \"deposit\", or \"withdraw\". "
+ " Problem token: " + token.toString(), lineNum);
throw new RuntimeException(msg);
}
}
// Returns a List of Strings and/or Longs. For example, for the line:
//
// deposit 20 java.lang.ArithmeticException
//
// This method will return a List containing Strings and Longs with the values:
//
// "deposit"
// 20L
// "java.lang.ArithmethidException"
//
private List getTokens(String line, int lineNum) {
List tokensList = new ArrayList();
int pos = 0;
// Eat any white space at the beginning
while (pos < line.length()) {
while (pos < line.length()
&& Character.isWhitespace(line.charAt(pos))) {
++pos;
}
// Recognize '-' as the start of a negative long value
if (Character.isDigit(line.charAt(pos)) || line.charAt(pos) == '-') {
int nextWhiteSpacePos = pos + 1;
while (nextWhiteSpacePos < line.length()
&& !Character.isWhitespace(line.charAt(nextWhiteSpacePos))) {
++nextWhiteSpacePos;
}
String longString = line.substring(pos, nextWhiteSpacePos);
try {
tokensList.add(new Long(longString));
}
catch (NumberFormatException e) {
// Runner will report this RuntimeException as
// with suiteAborted method invocation on the Reporter
throw new RuntimeException(
addLineNumber("Invalid long integer: "
+ longString + ".", lineNum));
}
pos = nextWhiteSpacePos;
}
else {
int nextWhiteSpacePos = pos + 1;
while (nextWhiteSpacePos < line.length()
&& !Character.isWhitespace(line.charAt(nextWhiteSpacePos))) {
++nextWhiteSpacePos;
}
String wordString = line.substring(pos, nextWhiteSpacePos);
tokensList.add(wordString);
pos = nextWhiteSpacePos;
}
}
return tokensList;
}
// Process a "newAccount" command from the script
private void processNewAccount(Reporter reporter, int lineNum) {
Report report = new Report(this, "ScriptDrivenAccountSuite.processNewAccount",
addLineNumber("About to create a new Account object.", lineNum));
reporter.testStarting(report);
try {
account = new Account();
report = new Report(this, "ScriptDrivenAccountSuite.processNewAccount",
addLineNumber("Created a new Account object.", lineNum));
reporter.testSucceeded(report);
}
catch (Exception e) {
String msg = addLineNumber("Account constructor threw an exception.",
lineNum);
report = new Report(this, "ScriptDrivenAccountSuite.processNewAccount",
msg, e);
reporter.testFailed(report);
}
}
// The getBalance command has two tokens, "getBalance" and a long integer
// expected return value, as in:
//
// getBalance 20
//
private void processGetBalance(Reporter reporter, List tokens, int lineNum) {
if (tokens.size() != 1) {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("getBalance commands must have 1 "
+ "argument, a long expected balance.", lineNum);
throw new RuntimeException(msg);
}
long expectedBalance = -1;
// The getBalance command has two tokens, a long value and an error message
Object o = tokens.get(0);
if (o instanceof Long) {
expectedBalance = ((Long) o).longValue();
}
else {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("The first argument of a getBalance "
+ "command must be a Long expected balance.", lineNum);
throw new RuntimeException(msg);
}
String msg = addLineNumber("About to process a getBalance command. "
+ "Expecting getBalance to return " + Long.toString(expectedBalance)
+ ".", lineNum);
Report report = new Report(this, "ScriptDrivenAccountSuite.processGetBalance",
msg);
reporter.testStarting(report);
long balance = account.getBalance();
if (balance == expectedBalance) {
msg = addLineNumber("The getBalance method returned "
+ Long.toString(expectedBalance) + ", as expected.", lineNum);
report = new Report(this, "ScriptDrivenAccountSuite.processGetBalance",
msg);
reporter.testSucceeded(report);
}
else {
msg = addLineNumber("The getBalance method returned "
+ Long.toString(balance) + ", when "
+ Long.toString(expectedBalance) + " was expected.", lineNum);
report = new Report(this, "ScriptDrivenAccountSuite.processGetBalance",
msg);
reporter.testFailed(report);
}
}
// The deposit command has two forms. The simpler form has two
// tokens, "deposit" and a long integer amount to deposit, as in:
//
// deposit 20
//
// The two token form of the deposit command causes this ScriptDrivenAccountSuite
// to deposit the long integer to the current Account object.
//
// The more complex form of the deposit command has three tokens, "deposit",
// a long integer amount to deposit, and the fully qualified name of an expected
// exception. For example:
//
// deposit -1 java.lang.IllegalArgumentException
//
private void processDeposit(Reporter reporter, List tokens, int lineNum) {
if (tokens.size() != 1 && tokens.size() != 2) {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("A deposit command "
+ "must have either 1 or 2 arguments.", lineNum);
throw new RuntimeException(msg);
}
// Both forms require a long amount to deposit as the first token
long amountToDeposit = -1;
Object o = tokens.get(0);
if (o instanceof Long) {
amountToDeposit = ((Long) o).longValue();
}
else {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("First argument to a deposit command "
+ "must be a long amount to deposit.", lineNum);
throw new RuntimeException();
}
if (tokens.size() == 1) {
String msg = addLineNumber("About to Deposit "
+ Long.toString(amountToDeposit) + ".", lineNum);
Report report = new Report(this, "ScriptDrivenAccountSuite.processDeposit",
msg);
reporter.testStarting(report);
try {
account.deposit(amountToDeposit);
msg = addLineNumber("Deposited " + Long.toString(amountToDeposit)
+ ".", lineNum);
report = new Report(this, "ScriptDrivenAccountSuite.processDeposit",
msg);
reporter.testSucceeded(report);
}
catch (Exception e) {
msg = addLineNumber("The deposit method threw an exception "
+ "when attempting to deposit " + Long.toString(amountToDeposit)
+ ".", lineNum);
report = new Report(this, "ScriptDrivenAccountSuite.processDeposit",
msg, e);
reporter.testFailed(report);
}
}
else {
// Parse the fully qualified exception name
String expectedName = null;
o = tokens.get(1);
if (o instanceof String) {
expectedName = (String) o;
}
else {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("Second argument to a deposit command "
+ "must be a String fully qualified exception name.", lineNum);
throw new RuntimeException(msg);
}
String msg = addLineNumber("About to Deposit "
+ Long.toString(amountToDeposit) + ".", lineNum);
Report report = new Report(this, "ScriptDrivenAccountSuite.processDeposit",
msg);
reporter.testStarting(report);
try {
account.deposit(amountToDeposit);
msg = addLineNumber("The deposit method returned normally, when "
+ expectedName + " was expected.", lineNum);
report = new Report(this, "ScriptDrivenAccountSuite.processDeposit",
msg);
reporter.testFailed(report);
}
catch (Exception e) {
String thrownName = e.getClass().getName();
if (thrownName.equals(expectedName)) {
msg = addLineNumber("The deposit method threw "
+ expectedName + ", as expected.", lineNum);
report = new Report(this, "ScriptDrivenAccountSuite.processDeposit",
msg);
reporter.testSucceeded(report);
}
else {
msg = addLineNumber("The deposit method threw "
+ thrownName + ", when "
+ expectedName + " was expected.", lineNum);
report = new Report(this, "ScriptDrivenAccountSuite.processDeposit",
msg);
reporter.testFailed(report);
}
}
}
}
// The withdraw command has two forms. The simpler form has two
// tokens, "withdraw" and a long integer amount to withdraw, as in:
//
// withdraw 20
//
// The two token form of the withdraw command causes this ScriptDrivenAccountSuite
// to withdraw the long integer from the current Account object.
//
// The more complex form of the withdraw command has three tokens, "withdraw",
// a long integer amount to withdraw, and the fully qualified name of an expected
// exception. For example:
//
// withdraw 10 com.artima.examples.account.ex6.InsufficientFundsException
//
private void processWithdraw(Reporter reporter, List tokens, int lineNum) {
if (tokens.size() != 1 && tokens.size() != 2) {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("A withdraw command "
+ "must have either 1 or 2 arguments.", lineNum);
throw new RuntimeException(msg);
}
// Both forms require a long amount to withdraw as the first token
long amountToWithdraw = -1;
Object o = tokens.get(0);
if (o instanceof Long) {
amountToWithdraw = ((Long) o).longValue();
}
else {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("First argument to a withdraw command "
+ "must be a long amount to withdraw.", lineNum);
throw new RuntimeException();
}
if (tokens.size() == 1) {
String msg = addLineNumber("About to withdraw "
+ Long.toString(amountToWithdraw) + ".", lineNum);
Report report = new Report(this, "ScriptDrivenAccountSuite.processWithdraw",
msg);
reporter.testStarting(report);
try {
long withdrawn = account.withdraw(amountToWithdraw);
if (withdrawn == amountToWithdraw) {
msg = addLineNumber("Withdrew " + Long.toString(amountToWithdraw)
+ ".", lineNum);
report = new Report(this,
"ScriptDrivenAccountSuite.processWithdraw", msg);
reporter.testSucceeded(report);
}
else {
msg = addLineNumber("The withdraw method returned "
+ Long.toString(withdrawn) + ", when "
+ Long.toString(amountToWithdraw)
+ " was expected.", lineNum);
report = new Report(this,
"ScriptDrivenAccountSuite.processWithdraw", msg);
reporter.testFailed(report);
}
}
catch (Exception e) {
msg = addLineNumber("The withdraw method threw an exception "
+ "when attempting to withdraw " + Long.toString(amountToWithdraw)
+ ".", lineNum);
report = new Report(this, "ScriptDrivenAccountSuite.processWithdraw",
msg, e);
reporter.testFailed(report);
}
}
else {
// Parse the fully qualified exception name
String expectedName = null;
o = tokens.get(1);
if (o instanceof String) {
expectedName = (String) o;
}
else {
// Runner will report this RuntimeException as with suiteAborted
// method invocation on the Reporter
String msg = addLineNumber("Second argument to a withdraw command "
+ "must be a fully qualified exception name.", lineNum);
throw new RuntimeException(msg);
}
try {
String msg = addLineNumber("About to withdraw "
+ Long.toString(amountToWithdraw) + ".", lineNum);
Report report = new Report(this,
"ScriptDrivenAccountSuite.processWithdraw", msg);
reporter.testStarting(report);
account.withdraw(amountToWithdraw);
msg = addLineNumber("The withdraw method returned normally, when "
+ expectedName + " was expected.", lineNum);
report = new Report(this,
"ScriptDrivenAccountSuite.processWithdraw", msg);
reporter.testFailed(report);
}
catch (Exception e) {
String thrownName = e.getClass().getName();
if (thrownName.equals(expectedName)) {
String msg = addLineNumber("The withdraw method threw "
+ expectedName + ", as expected.", lineNum);
Report report = new Report(this,
"ScriptDrivenAccountSuite.processWithdraw", msg);
reporter.testSucceeded(report);
}
else {
String msg = addLineNumber("The withdraw method threw "
+ thrownName + ", when "
+ expectedName + " was expected.", lineNum);
Report report = new Report(this,
"ScriptDrivenAccountSuite.processWithdraw", msg);
reporter.testFailed(report);
}
}
}
}
private String addLineNumber(String raw, int lineNum) {
return "Script file line " + Integer.toString(lineNum)
+ ": " + raw;
}
}
|
Sponsored Links
|