CompSci 308 Spring 2024 |
Advanced Software Design and Implementation |
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:
Please complete each step before moving onto the next.
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
null
, and empty, ""
, or blank, " "
true
and false
logic for conditionalsTest data can be provided via explicit data String
s, explicit data List
s, 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.
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:
@Test
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.
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:
@Test
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.
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):
makeButton()
method so that it that takes a String
parameter instead of an EventHandler
(so two different String
parameters)
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 String
s instead of Lambdas).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).makeButton()
method so that it that only takes one String
parameter (i.e., no extra parameter telling it what action to perform)
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.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.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"
). makeInputPanel()
in the constructor to take the list of keys.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.
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".