CS46B Lab 6

Copyright © Cay S. Horstmann 2010 - 2015 Creative Commons License
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License.

Modified by:

Instructions

Working in Pairs

Learning Outcomes

The parts marked optionallike this are optional. But you must finish all optional parts before leaving early.

A. Removing Directory Entries

  1. This lab continues from lab 2. If you finished lab 2, just clone lab2 project and rename it to lab5 (To clone: Right click on lab2 in the Package Explorer, Copy, then Paste. It will give you the option to rename). If you did not finish lab 2, start with this code and make it into a project lab5, following Step 1 of lab 1 instructions. You will also need the file deptdir.txt.

    Open a shell window. Run

    java -classpath /path/to/project/build/classes AddressBookDemo /path/to/deptdir.txt
    
    

    For</path/to/project/build/classes>: replace it with absolute path to your "bin" folder in the Eclipse project.
    For </path/to/deptdir.txt>: replace it with absolute path to your "input" file (deptdir.txt).

    If you use your own "lab2" project folder, make sure you don't use packages within your project definition.

    Be sure to supply the correct path to the directory holding the Eclipse project and the deptdir.txt files.

    What is the command line that you used to launch the program? (Tip: You may be able to paste it from your previous lab report.)

  2. Using the console UI, look up the phone number of your instructor. Then remove the entry.

    What happens? Why? (You'll need to read through the source code.)

OK, we'll need to implement the remove method. (Read through part B to do this part)

B. Designing the remove Method

  1. Back in Eclipse, read through the code of get and put in ArrayListAddressBook. With your buddy, formulate a plan for implementing remove.

    What is the pseudocode for your plan (i.e. as a mixture of English and Java)?

    You can call one of two remove methods of the ArrayList class.

  2. Discuss with your buddy what might go wrong when you implement your plan. Identify one or more risks in your strategy.

    From your prior programming experience, what kind of error do you and your buddy think you are most likely to make when implementing the remove method?

C. Implementing a JUnit Test Class

  1. Test-driven development is a method of development where you write a test first, before you even implement the code. We'll do that for the remove method, but first you need to know how to implement a test class.
  2. In Eclipse, locate your project in Package Explorer panel. Right click on the lab5 project and follow steps below to create a JUnit test case. SelectNew → Other...

    instructions

    Select JUnit → JUnit Test Case. Click Next.

    instructions

     

    In the next step, give the class name ArrayListAddressBookTest. The naming convention for a test class is to have the name of the class that is being tested, with the suffix Test. Make the selections circled in red.

    ""

    set up path

  3. Now it is time to provide a test case. Let's test something simple: An empty phone dictionary won't have an entry for Fred. Paste this method into your test class, ArrayListAddressBookTest, replacing the @Test test() method if it is present.
       @Test
    	 public void anEmptyDirectoryHasNoFred() 
    	 {
    	     AddressBook dir = new ArrayListAddressBook();
    	     assertNull(dir.get("Fred", "Phone"));
    	 }

    Note two things:

    Right-click on the ArrayListAddressBookTest class and select Run As → JUnit Test.

    What happens?

  4. The “green bar” means that you have reason to be happy. Your test passed.

    ""

    It is very easy to (a) add more tests and (b) re-run the tests every time you change your code. If the bar stays green, be happy. If it turns red, be happy too—it is much easier to fix a bug that you know how to reproduce than one that occurs in the wild.

D. Writing Tests for the remove method

  1. Test-driven design (TDD) means that we should write the tests first, then the feature.

    The process for writing a test: Come up with a descriptive method name and annotate it with @Test. Then put in some actions that should pass.

    Start with the bread-and-butter scenario first. Remove an entry that is present and see that it is no longer there. Follow this outline:

    Your unit test can only use the public API of the class that is being tested. (OK, technically, public or package-visible...) That's still better than the tests that we ran in the previous lab. Those tests were only able to use the user interface that the program made available to human users.

  2. Take a look at the remove method in the AddressBook interface. It returns a value. What is that value supposed to be if the entry is not present? Write a test that removes an entry from an empty directory and checks that the correct value is being returned.

    What is the code of your test method?

    This exercise shows one value of TDD. Writing a test case forces you to understand all aspects of the task that you are about to implement, before you start implementing it.

  3. After testing the basic scenario, consider typical edge cases. What should happen when you remove an entry that isn't present in a non-empty directory? Write a test case that tests that.

    What is the code of your test method?

  4. Now right-click on the ArrayListAddressBookTest class again and select Run As → JUnit Test.

    What happens? Why?

E. Implementing the remove Method

  1. That was not so surprising since the current remove method throws an exception. So now implement the remove method, following your plan of part B.

    What is the code for your remove method?

  2. Re-run the test class.

    Did all of your tests pass? If not, which ones failed?

  3. If your code didn't pass, fix it (or your test cases) until you get a green bar.

    Do you know that your code is now correct? Why or why not?

F. Changing the Implementation

  1. The remove method of the ArrayList class is not very efficient. It has to shift all the entries after the one being removed by 1 to prevent leaving an empty entry. Look at the example below:

    A B C X D E F →

    A B C D E F

    Since you don't care about the order, you could just move the last entry into the slot to be removed:

    A B C X D E F →

    A B C F D E

    Reimplement your remove method, using that implementation strategy.

    What is the code for your remove method now.

  2. Re-run the test class.

    Did all of your tests pass? If not, which ones failed?

    This is when a unit test suite shines. If you have the tests available, you are more likely to improve your code since testing it is so easy.

G. Testing Exceptions

  1. Some methods have as part of their contract that they throw an exception, and you want to be able to test that they really throw that exception when they are supposed to.

    For example, let us temporarily change the contract for remove, requiring that it throws a NoSuchElementException if you remove a non-existent entry.

    Change the implementation of your remove method

    What is the code for your remove method now.

  2. Re-run the test class.

    Did all of your tests pass? If not, which ones failed?

    Fix any failing tests before you go on.

  3. Now write a test case that simply removes a non-existent element, triggering the exception.

    What is the code for your test case?

  4. What happens when you run the test? Why?
  5. OK, that's not fair. The exception was supposed to happen. This isn't something you can check with an assert statement (unless you wrap it into a catch clause, which is really tedious).

    The designers of JUnit thought of this. Read through the documentation of the Test annotation.

    What do you do to fix up your test case?

  6. What happens now when you run the test? Why?

H. Test Fixtures

  1. When you write many test cases, they often need the same code for setting up the object under test. For example, when testing a directory, every test case is likely to construct a directory object. It is a good idea to make it into an instance variable of the test class:
    public class ArrayListAddressBookTest 
    	 {
    	 private AddressBook dir;
    	 ...
    	 }

    You need to initialize it somewhere. Look at the ArrayListAddressBookTest class again. When Eclipse created it, it provided several methods for you.

    What are they?

  2. You can initialize the phone directory in the method annotated with @Before. That method is run before every test case.
       @Before
    	 public void setUp() 
    	 {
    	     dir = new ArrayListAddressBook();
    	 }

    Reimplement your ArrayListAddressBookTest class in this way. Remove all local variables of type AddressBook or ArrayListAddressBook. Use an instance variable, and initialize it in the @Before method.

    What is the code of your ArrayListAddressBookTest class?

  3. Re-run the test class.

    Did all of your tests pass? If not, which ones failed?

    Fix any failing tests before you go on.

  4. Change the method name setUp to another name such as initialize or foo. Re-run the test class.

    What happens? Why? (Careful. The result may not be what you expect.)

    There are three other annotations. @After can be used to undo the effects of @Before. This is commonly used when testing file or database code. In the @Before method, you open a file or establish a connection, and you close it in the @After method. The @BeforeClass and @AfterClass methods are useful for doing setup that is required by multiple tests and doesn't need to be repeated before each individual test.

  5. Let's explore a more sophisticated use of @Before. Suppose that many of your tests want a dictionary that contains all entries of deptdir.txt. Add an instance variable deptDir to the ArrayListAddressBookTest class and add the code to load the entries:
    @Before
    public void setUp() 
    {
       dir = new ArrayListAddressBook();
       deptDir = new ArrayListAddressBook();
       deptDir.load("deptdir.txt");
    }

    Also add a test case

    @Test
    public void deptDirContainsHorstmann() 
    {
       assertNotNull(deptDir.get("Horstmann", "Phone"));
    }

    Run the test class. Does the new test case pass? If not, why not?

    Read the source code of get if you aren't sure.

  6. The problem is that get doesn't find deptdir.txt. Sadly, the method then silently makes an empty directory. This is actually very poor design that you should not do in your own code. Hiding a failure is always bad.

    OK, let's put the deptdir.txt file into your lab5 src directory, using the command line to get more command line practice. Use the cp (copy) command. In its simplest usage, it has the form

    cp sourceFile targetDirectory

    What is your full command?

    After copying deptdir.txt to your src folder, go to File > Refresh (F5).

    Another way to do this is dragging deptdir.txt to your src folder in Eclipse. If Eclipse, somehow, cannot locate deptdir.txt, and FileNotFoundException is caught, then providing full-path to that file could help solve the problem.

  7. Run the test again. What happens?
  8. At this point, you have two choices. Throw up your hands in disgust, or figure out what the IDE does behind your back.

    IDEs are wonderful, but they do things behind your back all the time. This is one reason to learn about command-line tools. Command-line tools give you much more control, and they act in a more predictable way.

    When the Java runtime executes the statement

    Scanner in = new Scanner(new File(source));

    and source is a relative path (i.e. one not starting with / or, in Windows, a drive letter followed by a colon), then it is evaluated relative to the “user directory”. That is normally the directory from which a program was started, but it is possible to change it. Eclipse changes it to the root of your project, as you can verify by adding

    System.out.println(System.getProperty("user.dir"));

    into the setUp method.

    Add the print statement and run your test class again. What directory is displayed?

  9. Change the call to load to src/deptdir.txt to properly specify the directory?
  10. Alternately, you can put deptdir.txt directly in the lab5 project folder.
  11. Run the test again. What happens?

I. Running JUnit from the Command Line

  1. Download junit-4.11.jar from http://github.com/junit-team/junit/downloads

    Using shell commands, make a directory /path/to/project/lib and copy the JUnit JAR file into that lib directory. As always, replace /path/to/project with the path on your system.

    What shell commands did you use?

  2. Using the cd command, change to the base directory of the project (i.e. /path/to/project). Then issue the command
    find .

    That is, the command find followed by a space and the period.

    The period denotes the current directory.

    What output do you get?

    Your output should show a directory hierarchy with directories src, test, lib, and build. If not, you didn't do the previous step correctly.

  3. Where did Eclipse put the class file for the ArrayListAddressBookTest class?
  4. You need to add three elements to the class path when running JUnit:

    Use this command line

    java -classpath build/test/classes:build/classes:lib/junit-4.11.jar org.junit.runner.JUnitCore ArrayListAddressBookTest
    Note that some system, for example, Windows system, use ";" instead of ":" when linking external libraries for classpath.
        

    What happens?

    If you don't get something indicating success, you did something wrong. Keep trying until you get it to work. To put it simply, the command above may only work with Netbeans, and I'm uncertain if it works even when using Netbeans.
    You may wonder what things that are passed to -classpath. Each token that is separated by colon ":" is path to directories that contain binary files of your java test cases and your java application binary files. The last token points to the JUnit library jar file.
    Next, org.junit.runner.JUnitCore is JUnit main class that contains the main method.
    Make sure you know the path to folders mentioned above before working on this command.

    Sorry, no green bar. You can use this command in a script that you can run automatically after compiling your code. This is a very common practice.

  5. In the invocation
    java -classpath ... org.junit.runner.JUnitCore ArrayListAddressBookTest

    what is the significance of the class org.junit.runner.JUnitCore?

    (You can get a hint by running java -classpath lib/junit-4.11.jar org.junit.runner.JUnitCore)