Sponsored Link •
|
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
|