The Artima Developer Community
Sponsored Link

ScriptDrivenAccountSuite Complete Listing

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



Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use