Example 3: Line Counter

SIGCSE 2006 Workshop Companion Web Site

Goal

In this example, we will be using another simple CS1-style problem to introduce you to some basic ideas in assignments requiring students to write their own tests, and then assessing how well they perform this task. For context, imagine that you are going to develop a simple class that can count the number of lines and characters read from an input source.

Note: If you are unable to complete this example, or if you are unfamiliar with Java syntax and need a bit of help, the LineCounter-final directory contains completed versions of the source files for this example.

Learning Objectives

Procedure

  1. Set up your IDE

    Follow the instructions in Example 1 if you need help. The remainder of these instructions are written for Eclipse users, but should be similar for other environments.

  2. Create a LineCounter project

    Again, follow the instructions in Example 1 to create a LineCounter project consisting of the files in the LineCounter directory. Feel free to collapse other project trees so they do not clutter your view.

  3. Open the LineCounter.java file

    The LineCounter class contains the partial implementation of a class that takes a buffered reader (which might be connected to a file, the console, a web page, or any other input source) and counts the number of lines and characters contained in the input source. Read the comments describing the methods to get a feel for how the class is intended to operate.

  4. Open the LineCounterTest.java file

    The LineCounterTest class is just a skeleton with a simple setUp() method provided. It does not yet contain any tests.

  5. Write a test case (almost!)

    In the spirit of test-driven development, we should write some test cases describing the behavior we expect before we start implementing the line counter's methods. To this end, let's imagine that we want to create a test case that counts some test input. Add this test case to the LineCounterTest class:

        public void testOneLine()
        {
            // Somehow get a buffered reader connected to an
            // input source containing a single line, say: "hello world!"
            // which is 12 characters long
            lineCounter.count( ... );
            assertEquals( 1, lineCounter.lines() );
            assertEquals( 12, lineCounter.characters() );
        }
    

    The question now is how to get the buffered reader. Certainly, we could create a file containing these characters as part of our project, but if we have to create a separate file for each test case, that will get annoying very fast.

  6. Create a helper for running tests

    Add a separate helper method to your LineCounterTest class that is designed to make it easy to convert simple text values in a test case into buffered readers that the line counter can operate on:

        // ----------------------------------------------------------
        /**
         * This helper method takes a string, creates a buffered reader that
         * can read from the string, and uses the test fixture's line counter to
         * count the lines/chars in the string using this buffered reader.
         * @param input the string to count
         */
        private void countString( String input )
        {
            try
            {
                BufferedReader in =
                    new BufferedReader( new StringReader( input ) );
                lineCounter.count( in );
                in.close();
            }
            catch ( IOException e )
            {
                e.printStackTrace();
            }
        }
    

    Note that this helper method combines the tasks of converting a string into a buffered reader, and then passing the result to the line counter's count() method.

  7. Complete your first test case

    Now we can add the missing parts ot the testOneLine test case:

        public void testOneLine()
        {
            countString( "hello world!" );
            assertEquals( 1, lineCounter.lines() );
            assertEquals( 12, lineCounter.characters() );
        }
    
  8. Write additional test cases of your own

    Using testOneLine as a model, write several additional test cases that you believe capture the expected operation of the LineCounter class. If you have a question about the desired behavior, raise it for the group to discuss!

    At this point, you are doing what a student would do, if they were required to write tests to turn in (although some students may choose to do things in a slightly different order!).

  9. Run your tests

    Select the LineCounterTest.java file in the left pane, and run your tests. At this point, they should all fail, since there is no implementation for the count() method yet.

  10. Implement the count() method

    Implement the count() method in the LineCounter class. Feel free to re-run your tests at any time to check your work so far. If you are new to Java and need some help with its input operations, read our I/O basics helper information.

  11. Submit your work

    At this point, we would normally have students submit their work to an automated grading system to check their work. We don't have one open for general submissions for exercises like this, but if you are interested in finding out about the system we use (and possibly trying it out on-line), then visit the Web-CAT Wiki.

Sidebar: Connecting to a File or URL

  1. The lesson: specify assignments to use generic input streams

    By phrasing the requirements for the LineCounter class so that it operates on streams, rather than on specific files (or the console!), we have decoupled the behavior we want implemented from the nature of the input source. This improves testability by allowing us to write simpler test cases that use the same stream-based interface for getting data into the class being written, but without requiring all the extra overhead that would be involved if we had written the assignment to require input on System.in, or in an external file named on the command line.

  2. How do you connect to files?

    If you want to extend the LineCounter class to handle files, you can add an extra method like this:

        // ----------------------------------------------------------
        /**
         * Open the given file and count the number of lines and characters
         * it contains.  The counts for this file are added to the running
         * totals stored in this line counter object.
         * @param fileName the name of the file to count
         */
        public void countFile( String fileName )
        {
            try
            {
                BufferedReader in =
                    new BufferedReader( new java.io.FileReader( fileName ) );
                count( in );
                in.close();
            }
            catch ( IOException e )
            {
                e.printStackTrace();
            }
        }
    

    This method is really just a wrapper around the count() method that takes a file name and constructs a buffered reader attached to the file.

  3. Connecting to a URL

    One advantage of decoupling the processing work being performed from the source of the input is that we can now apply the same class to different input sources. For example, we can write another method that works on URL parameters, opening the given web page and counting its contents:

        // ----------------------------------------------------------
        /**
         * Open the given URL and count the number of lines and characters
         * it contains.  The counts for this URL are added to the running
         * totals stored in this line counter object.
         * @param url the web page to count
         */
        public void countURL( String url )
        {
            try
            {
                BufferedReader in = new BufferedReader(
                    new java.io.InputStreamReader(
                        new java.net.URL( url ).openStream() ) );
                count( in );
                in.close();
            }
            catch ( IOException e )
            {
                e.printStackTrace();
            }
        }
    

    Decoupling the processing from the input source is critical for testing, but because it also allows us to add on extra features like this, we can also keep our assignments more relevant and interesting for students, rather than working with plain text files all the time.