CompSci 308
Spring 2024
Advanced Software Design and Implementation

Lab Coding Exercise : Testing (and Reflection)

In this lab, you will add JUnit tests to a forked version of the lab_browser project so that both the Model and View can be tested automatically. Then you will refactor the code to use Java's "advanced" reflection feature to dynamically create objects and call methods based on String values read from a configuration file.

In pairs within your SLogo teams, complete the following steps:

  1. Setup Pair Programming Environment
  2. Testing Discussion
  3. Writing Model Unit Tests
  4. Writing View Unit Tests
  5. Using Reflection to Create a Data Driven GUI
  6. Submit Your Work

Please complete each step before moving onto the next.

Resources

Discussion

As you program larger, more complex, projects it is vitally important, and some would say morally imperative, that you have confidence that the most basic aspects of your code works correctly based on automatically testing it (rather than occasional manual tests). JUnit focuses on testing individual methods. It is the most common automated test framework for Java and is built in to all major IDEs, including IntelliJ. (it has also inspired similar frameworks in almost every other modern language). It has even been important in changing developers habits, by practicing of test-driven development (TDD), so they produce tests along with their code features (and, better yet, before writing their code) instead of waiting until the code is complete.

It is impossible to guarantee code is completely free of bugs, but try to come up with a set of tests that check every line of code written works as intended. For example, check

Test data can be provided via explicit data Strings, explicit data Lists, or even special test resource data files. Each test should be documented as to their purpose (what specifically the input values are testing about the code), both with a descriptive name and with traditional comments. Resist the urge to simply create a lot of "random" tests (i.e., the shotgun approach) and focus on making each test as useful as possible (i.e., the sniper approach). Finally, make sure you know the expected output values for each test before you run the code.

Examine the existing browser code and discuss what would be useful to test to validate that both the NanoBrowserModel and NanoBrowserView classes works as expected. Consider different strategies for thinking of the kinds of values that would represent "happy" and "sad" possible code paths and write down as many test scenarios as you can using the GIVEN, WHEN, THEN format and including specific values for the Model and specific user actions in the View.

Before Moving On: Add your names and planned test scenarios to the DISCUSSION.md file, then add, commit the file, and push to your lab_browser repository.

Writing Model Unit Tests

Write the your pair's planned test scenarios the browser.model.NanoBrowserModel class. Switch the person who codes roughly every 2-3 tests to ensure each person gets plenty of time to navigate and code the tests. Run the tests regularly to make sure they all pass and to develop a working rhythm.

Based on the values and scenarios from your discussion, add tests to the class src/test/java/browser/model/NanoBrowserModelTest.java by writing new test methods:

Before Moving On: Run all your tests with test coverage to see close you came to checking every line (that does not guarantee the code is bug free, just that you did a reasonable job thinking of a variety of test cases). If the Line Coverage is less than 90%, look over the lines that were missed (marked by red in the gutter of IntelliJ's Editor window) and write new test cases to cover them.

Writing View Unit Tests

Write the your pair's planned test scenarios the browser.view.NanoBrowserView class. Switch the person who codes roughly every 2-3 tests to ensure each person gets plenty of time to navigate and code the tests. Run the tests regularly to make sure they all pass and to develop a working rhythm.

Based on the values and scenarios from your discussion, add tests to the class src/test/java/browser/model/NanoBrowserViewTest.java that extends DukeApplicationTest by writing new test methods:

After you have written all the tests you can think of, run them all again with test coverage to see how well you did checking every line (of course, that will not guarantee the code is bug free, just that you did a reasonable job thinking of a variety of test cases). If the Line Coverage is less than 60%, look over the lines that were missed (marked by red in the Editor window gutter within IntelliJ) and write new test cases that would cover them.

Before Moving On: Run all your tests with test coverage to see close you came to checking every line (that does not guarantee the code is bug free, just that you did a reasonable job thinking of a variety of test cases). If the Line Coverage is less than 60%, look over the lines that were missed (marked by red in the gutter of IntelliJ's Editor window) and write new test cases to cover them.

Using Reflection to Create a Data Driven GUI

Reflection is an amazing feature of many modern programming languages that lets you dynamically create objects and call methods based on String values instead of having to know their exact type names when the code is compiled. This flexibility lets you more easily hold their values in data structures or data files instead of hardcoding them directly within your code.

After each numbered step, run your tests to verify your refactoring did not break any functionality, just improved the design and switch the person who codes.

For these refactorings, the only Java code changes need to be made in the browser.view.NanoBrowserView class (other changes will involve making .properties files):

  1. Change the makeButton() method so that it that takes a String parameter instead of an EventHandler (so two different String parameters)
    • For this step, in each call to makeButton(), replace each Lambda that calls a single method with no parameters to just the name of that method (so it is still essentially hardcoded, but now using Strings instead of Lambdas).
    • Change the call to setOnAction() within the makeButton() method to use a new Lambda expression (instead of the one previously passed in) that uses the given string parameter to get a Method object and then call its invoke() method (and catches its many exceptions).
  2. Change the makeButton() method so that it that only takes one String parameter (i.e., no extra parameter telling it what action to perform)
    • For this step create a new properties file that maps the button's key name (the same one used in the original properties file) to the name of the method to invoke when it is pressed.
    • Create a second ResourceBundle instance variable and, in the NanoBrowserView class constructor, read the new properties file so that the method name to call is not hardcoded anywhere.
    • After loading both of the properties files, you should be able to use the one given string to look up both the text for the button to display (from the original ResourceBundle) and the action for the button to perform (from the new ResourceBundle). Again, this reduces the assumptions and what is hardcoded in your program.
  3. Create a data structure to represent the buttons in the Browser's panels
    • For this step create a List<String> instance variable that holds hardcoded values for the buttons' key names used in the properties files (i.e., it will hold the key strings "BackCommand", "NextCommand", and "GoCommand").
    • Change the call to the methods makeInputPanel() in the constructor to take the list of keys.
    • Change the code in the method to use a loop to create the buttons from the given list rather than hardcoding their order and number.

Note, it is typically likely better overall design to make an abstraction (substitutable subclasses) rather than methods, because that is more flexible and recognizable than this simple example. But in either case, reflection can be used to make it data driven rather than hardcoded.

Submitting your Work

At the end of lab, use Gitlab's Merge Request from your forked repository's main branch back to the original organization repository's main branch to submit your group's discussion summary and your test code.

Make sure the Title of your Merge Request is of the form "lab_testing - everyone's NetIDs".