Before You Begin
Pull from the skeleton to get a new lab04
folder.
This lab will begin with the installation of IntelliJ. While this is taking time to download, feel free to read / skim our Using IntelliJ Guide. You don’t need to read or internalize all of this to complete the lab. IntelliJ is complicated but the core features should feel somewhat familiar to text editors you have used in the past. When you do have more time, we strongly suggest going through the linked guides and videos to maximize your ability to use the IDE.
IntelliJ Setup
In the previous labs, you have been editing your Java code in a text editor of your choice and then compiling and running code through the terminal. Although this worked, the process was slightly disjointed and you had to switch back and forth between your editor and terminal. Additionally, most text editors do not have that much built in functionality to help you code in Java. Starting this week we will use IntelliJ which is an Integrated Development Environment or IDE. An IDE is a single program which combines typically a source code editor, compile / run tools, and a debugger. Some IDEs like IntelliJ contain even more features such as an integrated terminal and a graphical interface for Git commands. Finally IDEs also have tools like code completion which will help you write Java faster.
We will not be forcing you to use IntelliJ if you choose not to, but we recommend that all students do use it. The debugger and other features will make it well worth the time it takes to learn as they should make you a more efficient programmer and one that can solve problems on your own. From this point forward we will assume that you are using IntelliJ and if you do not we will not necessarily be able to offer the same support (e.g. if you use another program like VSCode for this class, our staff will not be able to help you with any problems like we would had you chosen to use IntelliJ).
Be mentally prepared to use a real world software development package. It will seem very complicated for a while, but we’ll lead you down the narrow path to success. Ask for help if you are stuck! It can be very hard to guess the right thing to do in IntelliJ. We also offer the IntelliJ WTFS Guide to solve some of the common problems.
Prerequisites
- Java 11, from Lab 1.
- You have successfully created your local repo for the class on your own machine. This is the
su20-s***
repository you made in Lab 1. - You have pulled from the skeleton, and you have a
lab04
directory in the same directory as your lab assignment folders.
Downloading and Installing IntelliJ
-
You’ll need to install the Community Edition of IntelliJ from the Jetbrains website. As a student you can actually get a student license for the Ultimate version, but there are not any additional features that we will use for this class. It is recommended and assumed that you proceed with the Community Edition.
-
After selecting the appropriate version for your OS (Mac OSX, Windows, or Linux), click download.
-
Run the install file and follow the prompts to install IntelliJ onto your computer. You can stick to all the default settings.
Again it might take a few minutes to download and install, so feel free to jump to the Using IntelliJ Guide.
Installing Plugins
We will now instruct you to install three plugins to help you add some CS61B specific functionality to IntelliJ. The three plugins are Python Community Edition (this will allow you to run python scripts from inside IntelliJ), Java Visualizer (this will allow you to see visualizations of your code similar to what you might see on python tutor), and CS 61B (includes among other things a style checker). Follow the steps below to install these plugins.
-
Start up IntelliJ. You might be asked to import settings, choose to not do this. Also if prompted accept the terms and conditions.
-
In the Welcome to IntelliJ IDEA window, click the Configure -> Plugins button on the bottom right of the window.
-
In the window that appears, enter “Python Community Edition” in the search bar at the top (if you do not see a search bar, make sure you are in the “Marketplace” tab). Select the “Python Community Edition” plugin by JetBrains and click the green Install button. Wait for the plugin to download and install.
-
Now repeat step 3, but instead install the “Java Visualizer” plugin by Eli Lipsitz.
-
Finally repeat step 3 once more, but instead install the “CS 61B” plugin by CS 61B Course Staff. If nothing comes up make sure you have a space in the name.
-
Once all of the plugins have installed, click the green Restart IDE button to finalize the installation.
Before you move on make sure that you have successfully added the Python Community Edition plugin, the Java Visualizer plugin, and the CS 61B plugin. You can do this by returning to the Configure -> Plugins menu again, then check the “Installed”.A few parts later in the lab will refer to the functionality of these plugins which will not exist unless you have correctly installed them.
Setting Up SDKs
There will be a shared structure to pretty much every programming assignment you will complete this semester so we will configure the default settings so that you should not have to do this for every new assignment.
-
If not already running, start up IntelliJ.
-
In the Welcome to IntelliJ IDEA window, click the Configure -> Structure for New Projects button on the bottom right of the window. After doing this step, you should see a window that looks like the following.
-
For the Project SDK option, you should select the option “11” from the list which corresponds to using Java 11. Once selected your screen should now look like the following.
-
Next we will add the Python SDK. Now navigate to the “Platform Settings -> SDKs” section in the sidebar. This should take you to a window that looks something like the below.
-
Click the ”+ -> Python SDK” button to add a new SDK. Once open select the “System Interpreter” option from the sidebar and you should be greeted with a screen that looks like the following.
It is possible that IntelliJ will already know where your python executable is, but if not you can click the “…“ option and then select the corresponding location of the
python3
executable (macOS and Linux) or thepython.exe
executable (Windows).If you are unsure where this might be try using the command
which python3
(macOS and Linux) orwhich python
(Windows) in your terminal. This should print out the print out the path for you!
After completing this step you should now see two SDKs, “11” and “Python 3.8” (if your python version differs that should be fine as long as it is Python 3.5+)
Setting Up Java Libraries
Remember the empty library-su20
folder? We are going to populate that folder with the Java libraries we need for this class. You will only need to do this once.
This section will involve a new Git topic called submodules. We do not expect you to know much of anything at all about submodules, but if you are curious feel free to read this short introduction.
- First, open up a terminal window and
cd
into your repository. -
Run
git submodule update --init
You should get output like this:
Submodule 'library-su20' (https://github.com/cs61bl/library-su20.git) registered for path 'library-su20' Cloning into '/mnt/c/Users/omatt/cal/cs61bl/su20-s1/library-su20'... Submodule path 'library-su20': checked out '1e28541f2c042fb7d72095ce5bfe4292b96bb1a7'
-
Ta-da! You now have libraries!
$ ls library-su20/ javalib/
If Git is having trouble running the
submodule
command and cloning thelibrary-su20
repository, try running the following commands to make sure the remote repository URL is correctly defined.git config --file=.gitmodules submodule.library-su20.url https://github.com/cs61bl/library-su20.git git submodule sync git submodule update --init
Below is shown the directory structure of library-su20
. Look inside the folder and make sure you see the eight .jar
files listed below. If you’re using your operating system’s file explorer the jar
part might not show up in the filenames, and that’s OK.
library-su20
└── javalib
├── algs4.jar
├── hamcrest-core-1.3.jar
├── jh61b.jar
├── junit-4.12.jar
├── stdlib-package.jar
├── stdlib.jar
├── ucb.jar
└── xchart-3.5.1.jar
Creating Projects
The following instructions apply for both this lab and for future assignments. Each time after pulling from skeleton
to get new lab or project files, you will need to run through the following steps again.
-
Start up IntelliJ.
-
Upon opening IntelliJ, click on the “Import Project” option. We will use “Import Project” rather than “Create New Project” throughout the semester which will make setup slightly easier as IntelliJ will automatically mark *.java files as source code.
-
Find and choose the directory of your current assignment (for today’s lab, it should be the
lab04
directory), then press the OK button. -
You should at this point be greeted with the main screen of IntelliJ which should look like the following. If you are greeted with any additional screens when setting up the project you can go ahead and select the default options for now.
If you open the file
lab04/DogTest.java
You should see that some of the words in the file are red, specificallyTest
andassertEquals
. If you mouse over them, you’ll see a message along the lines of “cannot resolve symbol”. The trouble is that we haven’t told IntelliJ where to find the CS 61B libraries we just pulled. We will resolve this in the next steps. -
Finally we will add the Java libraries. This among other things will allow IntelliJ to run JUnit tests which we will describe in more depth later in this lab. Navigate to the “File -> Project Structure” menu, then select “Libraries” in the sidebar. You should be greeted with a screen that looks like this.
-
Click the “+ -> Java” button, which should prompt you to select the file location. Navigate to your
su20-s***
repo and then find thelibrary-su20/javalib
folder within it. Select all of the*.jar
files (i.e. select all of the files that end in.jar
in thelibrary-su20/javalib
folder). After selecting them your window should look like the following.If prompted to add the library to the
lab04
module, select “Ok”. After these steps the libraries tab should look like the following with all of the.jar
files listed here.
At this point if things have been configured correctly each Java file should have a blue circle next to its name, and when you open the file DogTest.java
file you should see a green triangle near the line numbers on line 6.
IntelliJ Test
-
If you have not already, open
DogTest.java
. This time, the red text should be gone. If it is not, click File -> Project Structure -> Problems -> Fix -> Add to Dependencies -> OK.Try running the code by clicking Run -> Run, as shown below.
- This will probably pop up a very small dialog window like the one shown. Basically IntelliJ is saying that it isn’t quite sure what you mean by running the program and is giving you two choices:
0.
is to edit the configuration before running the program (which we won’t do).3.
is to run theDogTest
class, which is what we want.Some IntelliJ installations may come installed with an Android JUnit, which you can tell apart by the small green Android icon in place of the usual red-green left-right JUnit icon (in the screenshot below the Android JUnit is option 2). Make sure to choose the the regular, non-Android JUnit.
-
Click on
DogTest
and a green bar should appear with the message “All 2 tests passed.” as shown below.
You’ll notice after running your code that the green play and green bug icons in the upper right are now green; this is because IntelliJ remembered what you meant by “Run” for this project and you can now click this button to run your program. You’ll learn more about this over time as we use more advanced features of IntelliJ.
If you are able to run and pass these tests that means that you have set up IntelliJ correctly and you should be able to continue on with the lab. If you cannot run, that is totally fine IntelliJ is a complicated piece of software. Try to reread the above steps to see if you missed anything, or ask a TA, tutor, or AI for more assistance.
One common error is that the SDK is not properly selected. To fix this, go to File -> Project Structure -> Projects and make sure that the selected project SDK is Java 11 not python 3.8 (the option should just be called “11”).
Optional: Embedded Terminal
IntelliJ has the cool feature that you can have a working terminal in the workspace so you don’t have to constantly switch between having IntelliJ and your terminal, if that becomes necessary for whatever reason.
For Mac users, you should be able to skip this setup section. Windows users will likely have to put in a little leg work. This setup assumes you are a Windows user and you have Git Bash installed.
-
First, find the preferences/settings tab and select it. (Or use Ctrl+Alt+S.)
-
Type in “terminal” in the search bar. Once there, type in
"C:\Program Files\Git\bin\bash.exe"
into the Shell Path field. Click OK. -
To test if you’ve properly set this up, hover over the little box in the bottom left corner and select terminal; the bottom third of your screen should now be a terminal, the equivalent of having git bash right there. Try typing something in! If you’re able to run basic commands like
ls
orcd
orecho 'Hello world'
you’ve done it!
IntelliJ Debugger Basics
This section will walk you through three different debugging exercises which will introduce you to the IntelliJ debugger. This is one of the single most important features of IntelliJ. The debugger will allow you to hopefully resolve bugs in your code more efficiently.
That being said these exercises are not graded in the autograder for this lab, but if you skip them, you will set yourself back.
Breakpoints and Step Into
We’ll start by running the main method in DebugExercise1
.
Open up this file in IntelliJ and click the Run button. You should see three statements printed to the console, one of which should strike you as incorrect. If you’re not sure how to run DebugExercise1
, right click on it in the list of files and click the Run DebugExercise1.main
button as shown below:
Somewhere in our code there is a bug, but don’t go carefully reading the code for it! While you might be able to spot this particular bug, often bugs are nearly impossible to see without actually trying to run the code and probe what’s going on as it executes.
Many of you have had lots of experience with using print statements to probe what a program is thinking as it runs. At times, print statements can be useful, especially when trying to search for where an error is occurring. However, they have a few disadvantages:
- They require you to modify your code (to add print statements). Particularly nasty bugs are those whose behavior changes after print statements are added sometimes called heisenbugs.
- They require you to explicitly state what you want to know in advance (since you have to say precisely what you want to print).
- They provide their results in a format that can be hard to read, since it’s usually just a big blob of text in the execution window. This is especially true for print statements in loops or recursive functions.
Often (but not always) it takes less time and mental effort to find a bug if you use a debugger. The IntelliJ debugger allows you to pause the code in the middle of execution, step through the code line by line, and even visualize the organization of complex data structures like linked lists with the same diagrams that would be drawn by the Online Java Visualizer.
While they are powerful, debuggers have to be used properly to gain any advantage. We encourage you to do what one might call “scientific debugging”, debugging by using something quite similar to the scientific method!
Generally speaking, you should formulate hypotheses about how segments of your code should behave, and then use the debugger to resolve whether those hypotheses are true. With each new piece of evidence, you will refine your hypotheses, until finally, you cannot help but stumble right into the bug.
Our first exercise introduces us to two of our core tools, the Breakpoint and the Step Over button. In the left-hand Project view, right click (or two finger click) on the DebugExercise1
file and this time select the Debug option rather than the Run option. If the Debug option doesn’t appear, it’s because you didn’t properly import your lab04
project. If this is the case, repeat the lab IntelliJ setup instructions above.
You’ll see that the program simply runs again, with no apparent difference! That’s because we haven’t give the debugger anything interesting to do. Let’s fix that by setting a breakpoint.
To set a breakpoint, scroll to the line that says int t3 = 3;
, then left click just to the right of the line number. You should see a red dot appear that vaguely resembles a stop sign, which means we have now set a breakpoint.
If we run the program in debug mode again it’ll stop at that line. If you’d prefer to avoid right-clicking to run your program again, you can click the bug icon in the top right of the screen instead.
If the text console (that says things like round(10/2)
) does not appear when you click the debug button, you may need to perform one additional step before proceeding. At the top left of the information window in the bottom panel, you should see two tabs labeled Debugger and Console. Click and drag the Console window to the far right of the bottom panel. This will allow you to show both the debugger and the console at the same time.
Once you’ve clicked the debug button (and made your console window visible if necessary), you should see that the program has paused at the line at which you set a breakpoint, and you should also see a list of all the variables at the bottom, including t
, b
, result
, t2
, b2
, and result2
. We can advance the program one step by clicking on the “step into” button, which is an arrow that points down as shown below:
We’ll discuss the other buttons later in this lab.
Make sure you’re pressing Step Into rather than Step Over. Step into points down, whereas step over looks more like a triangle.
Each time you click this button, the program will advance one step.
Before you click each time, formulate a hypothesis about how the variables should change.
Repeat this process until you find a line where the result does not match your expectations. Then, try and figure out why the line doesn’t do what you expect. If you miss the bug the first time, click the stop button (red square), and then the debug button to start back over. Optionally, you may fix the bug once you’ve found it.
Step Over and Step Out
Just as we rely on layering abstractions to construct and compose programs, we should also rely on abstraction to debug our programs. The “step over” button in IntelliJ makes this possible. Whereas the “step into” from the previous exercise shows the literal next step of the program, the “step over” button allows us to complete a function call without showing the function executing.
The main method in DebugExercise2
is supposed to take two arrays, compute the element-wise max of those two arrays, and then sum the resulting maxes. For example, suppose the two arrays are {2, 0, 10, 14}
and {-5, 5, 20, 30}
. The element-wise max is {2, 5, 20, 30}
, and the sum of this element-wise max is 2 + 5 + 20 + 30 = 57.
There are two different bugs in the provided code. Your job for this exercise is to fix the two bugs, with one special rule: You should NOT step into the max
or add
functions or even try to understand them.
These are very strange functions that use syntax (and bad style) to do easy tasks in an incredibly obtuse way. If you find yourself accidentally stepping into one of these two functions, use the “step out” button (an upwards pointing arrow) to escape.
Even without stepping INTO these functions, you should be able to tell whether they have a bug or not. That’s the glory of abstraction! Even if I don’t know how a fish works at a molecular level, there are some cases where I can clearly tell that a fish is dead.
Now that we’ve told you what “step over” does, try exploring how it works exactly and try to find the two bugs. If you find that one of these functions has a bug, you should completely rewrite the function rather than trying to fix it.
If you’re having the issue that the using run (or debug) button in the top right keeps running
DebugExercise1
, right click onDebugExercise2
to run it instead.
If you get stuck or just want more guidance, read the directions below.
Further Guidance (for those who want it)
To start, try running the program. The main
method will compute and print an answer to the console. Try manually computing the answer, and you’ll see that the printed answer is incorrect. If you don’t know how to manually compute the answer, reread the description of what the function is supposed to do above, or read the comments in the provided code.
Next, set a breakpoint to the line in main
that calls sumOfElementwiseMaxes
. Then use the debug button, followed by the step-into function to reach the first line of sumOfElementWiseMaxes
. Then use the “step over” button on the line that calls arrayMax
. What is wrong with the output (if anything)? How does it fail to match your expectations?
Note that to see the contents of an array, you may need to click the rightward pointing triangle next to the variable name in the variables tab of the debugger window in the bottom panel.
If you feel that there is a bug, step into arrayMax
(instead of over it) and try to find the bug.
Reminder: do not step into
max
. You should be able to tell ifmax
has a bug using step over. Ifmax
has a bug, replace it completely.
Repeat the same process with arraySum
and add
. Once you’ve fixed both bugs, double check that the sumOfElementwiseMaxes
method works correctly for the provided inputs.
Conditional Breakpoints and Resume
Sometimes it’s handy to be able to set a breakpoint and return to it over and over. In this final debugging exercise, we’ll see how to do this and why it is useful.
Try running DebugPractice3
, which attempts to count the number of turnips available from all grocery stores nearby. It does this by reading in foods.csv
, which provides information about foods available, where each line of the file corresponds to a single product available at a single store. Feel free to open the file to see what it looks like. Strangely, the number of turnips seems to be negative.
Set a breakpoint on the line where totalTurnips = newTotal
occurs, and you’ll see that if you “step over”, the total number of turnips is incremented as you’d expect. One approach to debugging would be to keep clicking “step over” repeatedly until finally something goes wrong. However, this is too slow. One way we can speed things up is to click on the “resume” button (just down and to the left from the step-over button), which looks like a green triangle pointing to the right. Repeat this and you’ll see the turnip count incrementing repeatedly until something finally goes wrong.
An even faster approach is to make our breakpoint conditional. To do this, right (or two-finger) click on the red breakpoint dot. Here, you can set a condition for when you want to stop. In the condition box, enter “newTotal < 0”, stop your program, and try clicking “debug” again. You’ll see that you land right where you want to be.
See if you can figure out the problem. If you can’t figure it out, talk to your partners, another partnership, or a lab assistant.
Recap: Debugging
By this point you should understand the following tools:
- Breakpoints
- Stepping over
- Stepping into
- Stepping out (though you might not have actually used it in this lab)
- Conditional breakpoints
- Resuming
However, this is simply scratching the surface of the features of the debugger! Feel free to experiment and search around online for more help.
Remember that Watches tab? Why not read into what that does?
Or try out the incredibly handy Evaluate Expressions calculator button (the last button on the row of step into/over/out buttons)?
Or perhaps look deeper into breakpoints, and Exception Breakpoints which can pause the debugger right before your program is about to crash.
We won’t always use all of these tools, but knowing that they exist and making the debugger part of your toolkit is incredibly useful.
Testing Your Code with JUnit
In the rest of the lab, you will be introduced to JUnit. JUnit provides a way to write repeatable tests, which substantially reduces the tedium of testing your code. Many of your lab submissions for the rest of the course will include a JUnit testing file and all of our autograders are written using JUnit.
JUnit also makes easy an approach to programming called test-driven development (TDD). TDD is popular approach in industry in which you design test cases before the code they are testing. We will encourage it in the remainder of CS 61BL, starting by leading you through the steps of the construction of a class representing measurements (feet, yards, inches).
JUnit Framework
As you might have seen in our DogTest
example from before, JUnit is a testing framework that’s integrates nicely into the IntelliJ programming environment. Each of the test functions is written using a number of assertion methods provided by the JUnit framework. Some of the most useful methods provided by JUnit are the following:
method | description |
---|---|
void assertTrue (boolean condition); | If assertTrue ’s condition evaluates to false, then the test fails. |
void assertTrue (String errmsg, boolean condition); | Like assertTrue , but if it fails, it prints an error message. |
void assertNull (Object obj); | If assertNull ’s argument isn’t null , the test fails. An error message may also be supplied. |
void assertNotNull(Object obj); | Fails for a null argument. An error message may be supplied. |
void assertEquals (Object expected, Object actual); | assertEquals succeeds when expect.equals(actual) . For primitives, it checks expected == actual . An error message may also be supplied. |
void fail() ; | If this code is run, the test fails. An error message may be supplied. |
JUnit Example
Suppose we have written a toString
method for a Line
class and want to test that it works correctly. The constructor for this class has the signature Line(int x1, int y1, int x2, int y2)
. We expect a Line
object to print out in the form (x1, y1), (x2, y2)
. Here’s an example method using JUnit that could test this functionality:
void testToString(){
Line l = new Line(8, 6, 4, 2);
assertEquals("(8, 6), (4, 2)", l.toString());
}
Using IntelliJ to Write JUnit Tests
Similar to the debugging exercises, this section will not be graded, but it is recommended that you complete this exercise either now or sometime over the next few days. JUnit testing is an equally important skill to learn as it will be used extensively throughout the rest of our class (in labs, projects, and exams).
Past that, in industry testing your code is a huge part of what you will do as a software engineer. Writing code is incomplete without a solid set of tests to verify its fault tolerance and accuracy.
One of the many great features about IntelliJ is that it can be used to start generating JUnit tests. We will illustrate how it can be used with the following example. Follow along each of the steps in IntelliJ.
-
Navigate to the
Counter.java
. -
Make a new JUnit Test Case:
-
Click on the class name in the
Counter.java
file and select “Navigate -> Test”. Alternatively, you can use CTRL + Shift + T (CMD + Shift + T on a Mac) -
Click “Create New Test…“. If you are asked to create test in the same source root, click “Ok”.
-
Name the JUnit Test Case
CounterTest
. Select “JUnit 4” as the testing library. Next check the boxes for theincrement()
andreset()
functions.
- You should see a file similar to the following:
import org.junit.Test; import static org.junit.Assert.*; public class CounterTest { @Test public void increment() { } @Test public void reset() { } }
- Edit your
CounterTest.java
to look like the class definition below.
import static org.junit.Assert.*; public class CounterTest { @org.junit.Test public void testConstructor() { Counter c = new Counter(); assertTrue(c.value() == 0); } @org.junit.Test public void testIncrement() throws Exception { Counter c = new Counter(); c.increment(); assertTrue(c.value() == 1); c.increment(); assertTrue(c.value() == 2); } @org.junit.Test public void testReset() throws Exception { Counter c = new Counter(); c.increment(); c.reset(); assertTrue(c.value() == 0); } }
-
-
Run your JUnit Test Case. Similar to before you should be able to Run your JUnit test, and all tests will pass.
-
We have shown you what it looks like to pass a test, but what happens if you fail? Intentionally introduce an error into one of the
CounterTest
methods, asserting for example that the value of a freshly-builtCounter
object should be 7. Run the JUnit test again and observe the error messages that result.
Testing Principles
Test-driven Development
Test-driven development is a development process that involves designing test cases for program features before designing the code that implements those features. The work flow is:
- Write test cases that demonstrate everything you want your program to be able to do. In this state most tests should fail and that is fine.
- Write as little code as possible so that all the tests are passed.
- Clean up the code as necessary. Recheck that all tests still pass.
Statement Coverage
One testing principle you can imagine is that test values should exercise every statement in the program, since any statement that’s not tested may contain a bug. Recall the leap year program from Lab01, below is a example of how it might be implemented:
public static boolean isLeapYear(int year) {
if (year % 400 == 0) {
return true;
} else if (year % 100 == 0) {
return false;
} else if (year % 4 == 0) {
return true;
} else {
return false;
}
}
The code contains four cases, exactly one of which is executed for any particular value of year. Thus we must test this code with at least one year value per case, so at least four values of year are needed for testing:
- a year that’s divisible by 400;
- a year that’s divisible by 100 but not by 400;
- a year that’s divisible by 4 but not by 100;
- a year that’s not divisible by 4.
This approach by itself is insufficient as we will see below.
Path Coverage
To augment this first principle, we’ll say we need to test various paths through the program. For example, suppose our program had two consecutive if
statements:
if ( ... ) {
...
}
if ( ... ) {
...
}
There are two possibilities for each if case: true
or false
. Thus there are four paths through the two statements, corresponding to the four possibilities
true
,true
true
,false
false
,true
false
,false
This explains why one test is insufficient to exercise all the code in the following example.
A
year
value of 2000 causes all the statements in this program segment to be executed. However, there is a bug in this code it will not catch.
public static boolean isLeapYear(int year) {
isLeapYear = false;
if (year % 4 == 0) {
isLeapYear = true;
}
if (year % 100 == 0) {
isLeapYear = false;
}
if (year % 400 == 0) {
isLeapYear = true;
}
}
From the previous discussion, it looks like we need eight tests, corresponding to the eight paths through the three tests. They are listed below.
year % 4 == 0, year % 100 == 0, and year % 400 == 0 // (which just means that year % 400 == 0)
year % 4 == 0, year % 100 == 0, and year % 400 != 0
year % 4 == 0, year % 100 != 0, and year % 400 == 0 // (not possible)
year % 4 == 0, year % 100 != 0, and year % 400 != 0
year % 4 != 0, year % 100 == 0, and year % 400 == 0 // (not possible)
year % 4 != 0, year % 100 == 0, and year % 400 != 0 // (not possible)
year % 4 != 0, year % 100 != 0, and year % 400 == 0 // (not possible)
year % 4 != 0, year % 100 != 0, and year % 400 != 0 // (equivalently, year % 4 != 0)
Notice that some of the tests are logically impossible, and so we don’t need to use them. This leaves the four tests we needed to write.
Testing Loops
Loops can vastly increase the number of logical paths through the code, making it impractical to test all paths. Here are some guidelines for testing loops, drawn from Program Development in Java by Barbara Liskov and John Guttag, a book used in previous CS 61B offerings.
- For loops with a fixed amount of iteration, we use two iterations. We choose to go through the loop twice rather than once because failing to reinitialize after the first time through a loop is a common programming error. We also make certain to include among our tests all possible ways to terminate the loop.
- For loops with a variable amount of iteration, we include zero, one, and two iterations, and in addition, we include test cases for all possible ways to terminate the loop. The zero iteration case is another situation that is likely to be a source of program error.
Liskov and Guttag also say: This approximation to path-complete testing is, of course, far from fail-safe. Like engineers’ induction “One, two, three—that’s good enough for me,” it frequently uncovers errors but offers no guarantees.
Black-box Testing
All the testing principles discussed so far focused on testing features of the code. Since they assume that we can see into the program, these techniques are collectively referred to as glass-box testing, as if our code is transparent.
Another testing approach is called black-box testing. It involves generating test cases based only on the problem specification, not on the code itself. There are several big advantages of this approach:
-
The test generation is not biased by knowledge of the code. For instance, a program author might mistakenly conclude that a given situation is logically impossible and fail to include tests for that situation; a black-box tester would be less likely to fall into this trap.
-
Since black-box tests are generated from the problem specification, they can be used without change when the program implementation is modified.
-
The results of a black-box test should make sense to someone unfamiliar with the code.
-
Black-box tests can be easily designed before the program is written, so they go hand-in-hand with test-driven development.
In black-box testing as in glass-box testing, we try to test all possibilities of the specification. These include typical cases as well as boundary cases, which represent situations that are extreme in some way, e.g. where a value is as large or as small as possible.
There are often a variety of features whose “boundaries” should be considered. For example, in the DateConverter
program, boundary cases would include not only dates in the first and last months of the year, but also the first and last dates of each month, etc.
Whenever you write a program, try to think of any boundary cases. These cases although potentially rare, are a common source of error. The safest thing to do is brainstorm as many unique ones as you can then write tests which test each unique boundary case.
Test Driven Development Illustrated: Mod \(N\) Counters
Yet again, the
ModNCounter
exercises covered here will not be graded. You should attempt them if you have time in lab to complete this, otherwise you should skim through this section to see what test driven development looks like in practice.
Mod \(N\) Counters Defined
Now that we’ve covered some basics of how to use JUnit and some testing principles, we’ll use an example of a class that implements a Mod \(N\) counter to demonstrate good testing practices.
For our purposes, a Mod \(N\) counter is a counter that counts up to a specified amount (the \(N\)), and then cycles back to zero. For example, if we had a Mod 4 counter, it would count like this: 0, 1, 2, 3, 0, 1, 2, 3, 0, …
The ModNCounter
class is similar to the Counter
class, but notice that in order to keep track of the value of \(N\) it will need an an extra instance variable—a good name for it is myN
. myN
is initialized with a one-argument constructor whose argument is the intended myN
value. Thus the following code should initialize a Mod \(N\) counter with \(N=2\) and print 0, then 1, then 0.
ModNCounter c = new ModNCounter (2);
System.out.println(c.value());
c.increment();
System.out.println(c.value());
c.increment();
System.out.println(c.value());
c.increment();
Exercise: Renaming a Class in IntelliJ
This time to write the ModNCounter
class, we’re going to modify the Counter
class from earlier. Again open the Counter.java
program you just tested. Right-click “Counter” within the Java code and select “Refactor —> Rename”. Next type ModNCounter
in the little box that appears. When you do this, a dialog will likely pop up asking if you want to rename CounterTest as well. Select “Ok”.
The effect of this change is to change any reference to
Counter
toModNCounter
, not only inCounter.java
but also inCounterTest.java
(or any other Java files within the project).
You should notice that the name of the file Counter.java
is now ModNCounter.java
and the name of the file CounterTest.java
is now ModNCounterTest.java
. In addition, all references to these classes have been changed appropriately. Don’t make any other changes to ModNCounter.java
just yet.
Remember this refactoring operation! It’s pretty common to want to rename an identifier (a variable or a class name) at some point. IntelliJ makes this easy by renaming not only the definition of the identifier but also everywhere it’s used.
Exercise: Test-driven Development for ModNCounter
Here’s a walk through of how to do test-driven development for creating ModNCounter
:
-
Decide what you’re going to change in your program: In
ModNCounterTest.java
, supply an argument for each of the constructor calls, because you know you will have to initialize aModNCounter
with some value ofN
. -
Write code in JUnit to test the change: Add code in
testIncrement
that checks (using assertions) that wraparound of the cycling counter works correctly. -
Eliminate compilation errors: In
ModNCounter.java
, add an argument to the constructor. Both files should now be free of compiler errors. Don’t make any other changes inModNCounter.java
. We don’t want it to work just yet. RunModNCounterTest
. All tests should pass except the wraparound check. -
Write code to make the all of the tests pass: Now go fix the
increment
method and run the JUnit test again. If all goes well, your updated code should pass all the tests.
So you’ve done some test-driven development! First, you supplied test cases in the ModNCounterTest
class. Then you changed as little code as possible in ModNCounter
to remove compile-time errors. Then you provided the code to pass the tests.
Parting Advice on Testing
As you progress through the course you will hopefully improve your testing skills! Here are some last bits of advice for now.
-
Write tests as if you were testing your worst enemy’s code. You’re generally too familiar with your own code and might read a given line as containing what you meant to write rather than what you did write. Don’t fall into the trap of hoping not to find bugs in your own code.
-
Test your program with values that are as simple as possible. If the program is supposed to work for a set of 1000 data values, make sure it works on a set of 3 values first.
-
Wrapping a loop around your code may allow you to test it with multiple values in a single run.
-
Make sure you know how your program is supposed to behave on a given set of test data. Often lazy programmers try a test and just scan through it thinking that it “looks right”. Such a programmer might later be embarrassed to find out that they computed a product cost that’s greater than the national debt or a quantity that’s greater than the number of atoms in the universe.
-
Make sure to cover both the common cases and the extreme, edge or boundary cases. Forgetting one or the other (or both!) can cause you to miss critical bugs in your code.
Exercise: Testing a Measurement Class
Unlike the previous exercises, this section will be graded. All points for this lab will be derived from completing the
Measurement
class and from writing the corresponding tests inMeasurementTest.java
.
Now, you’re going to be writing the code and a JUnit Test Case (a whole file of tests) for the Measurement
class. We have provided “stubs” for each of the methods in Measurement.java
. Stubs show the header of the method. We have also included an empty MeasurementTest.java
which is where you should write your JUnit tests. You will both be writing the code for these methods and, we hope, doing test-driven development (write the tests first).
You should follow this process for this exercise:
- Read over the
Measurement.java
class to understand how the class should work. The comments above each of the methods and constructors explain what the expected behavior of this class will be. Discuss this with your partner to make sure that you both understand this completely before continuing on to the next step. - Write JUnit tests in the
MeasurementTest.java
file. You should write tests which allow you to test all of the methods / behavior in the class. - Run the tests in
MeasurementTest.java
. As you still have not implemented the code inMeasurement.java
, you should fail these tests. - Write code to make the all of the tests pass. If you fail some of your tests, make sure that the test is correct and if it is then proceed to debug your tests until you pass.
As mentioned in lab and lecture, all your instance variables should be private. For this exercise you are not to change the public interface of the
Measurement
class; e.g. don’t add any additional public getter methods. You are allowed to add anything that’s private.
Recap
In this lab, we discussed,
- Stepping into, over, and out inside the IntelliJ debugger.
- JUnit Testing Framework.
- Test driven development and good testing principles.
Deliverables
Your work will be graded on two criteria:
- the correctness of your
Measurement.java
- whether or not you have written 1+ tests in
MeasurementTest.java
which you pass.
For this assignment we expect you to do all testing on your own, so you should not rely on the autograder to check your work. We have intentionally increased the length of time that it takes tokens to recharge and decreased the number of tokens that you have. You should only submit to the autograder once you have finished all testing on your own.
Additionally if you have not already, you should complete the other non-graded exercises throughout the lab. By the start of next week try to have read through and completed these exercises. We will start to rely heavily on the concepts covered in this very long lab and you will benefit greatly from investing the time early to understand the debugging and testing.
Frequently-Asked Questions
- Things like
String
orString.equals()
are red! - This is a JDK issue, go to File > Project Structure > Project > Project SDK to troubleshoot. If your Java version is 11.0, then you should have a 11.0 SDK and a Level 11 language level.
- Things like
@Test
are red! - You probably forgot to add your libraries. You have to add your libraries every time you start a new project!
- Console button isn’t showing up!
- That’s probably because you didn’t compile successfully. Usually, it’s because you did not add your libraries.
- Java files have a red circle, with a J inside the circle, next to the file icon
- Right-click the folder containing that Java file, then Mark as -> Sources Root.
Measurement.java
- Do we need to handle negative numbers?
- No. The
Measurement
class doesn’t need to do sensible things when passed negative numbers as arguments. - Do we need to worry about integer overflow?
- No. (Although you could imagine someone’s Measurement class handling this correctly.)
- Do we need to worry about
null
arguments? - Not this time. You may assume that
minus
andplus
are only passed non-null arguments. (They are the only two methods that take in objects.) - Should the methods modify the current
Measurement
object or create a new Measurement object to be returned? minus
,plus
, andmultiple
should all create a newMeasurement
object.