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
- Exposure to writing tests dealing with I/O
- Familiarity with requiring students to write tests as part of an assignment
- Familiarity with using reference tests to assess student code
- Exposure to measuring test coverage over student code
- Exposure to using an automated grading system to assess student-written tests
Procedure
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.
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.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.Open the LineCounterTest.java file
The
LineCounterTest
class is just a skeleton with a simplesetUp()
method provided. It does not yet contain any tests.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.
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.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() ); }
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 theLineCounter
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!).
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 thecount()
method yet.Implement the count() method
Implement the
count()
method in theLineCounter
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.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
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 onSystem.in
, or in an external file named on the command line.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.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.