Lesson Weekend

Setup

JUnit 4 is the testing suite we will use, not only is it the gold standard for Java testing, but it's also built right into IntelliJ! Let's walk through how to set this up.

  • Open your Ping Pong project in IntelliJ.

  • Then, open the PingPong.java file - it is currently just an empty class.

  • We could make our test files by hand, but we can actually let IntelliJ help us out here. Click between the curly braces of the class, and activate the shortcut alt + shift + t(you may need to try ctrl + shift + t or command + shift + t instead).

  • Choose Create new test.

  • Leave all default settings, and click OK.

IntelliJ will generate a new test file in the src/test/java directory called PingPongTest.java. This is pretty cool! We don't have to fuss with filenames and directory structure to set up our tests! It even imports code from the JUnit library for us, and creates the PingPongTest.java class.

Sweet!

Identifying the Simplest Behavior

Now, don't forget about the Red, Green, Refactor workflow! In fact, it's a good idea to keep that lesson open as you follow along.

Before we write a coded test, we must identify the simplest possible behavior our program must exhibit.

Here's what a Ping Pong application does: We enter a number. It counts up to that number, replacing numbers divisible by 3 with "ping", numbers divisible by 5 with "pong" and numbers divisible by both with "ping pong".

So, what is the simplest possible behavior such a program must demonstrate?

The primary function of this application is to replace certain numbers with strings. But what behaviors will it exhibit that are even simpler than that? First off, it must be able to count to the provided number before replacing any numbers. And the simplest number our program will have to count to is 1.

Our first, simplest possible behavior the program will exhibit is counting to 1.

Anatomy of a JUnit Test

We can now begin the second step in the Red, Green, Refactor workflow. We'll write a coded test for the simplest behavior we've just identified.

Our tests reside in the PingPongTest class declaration. Type out the following:

src/test/java/PingPongTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class PingPongTest {

  @Test
  public void runPingPong_countUpToOne_ArrayList() {
    // contents of test will eventually go here. 
  }
}

Every test begins with @Test. This is called an annotation. Annotations are marked with a @ symbol. They are used to provide extra information to the Java compiler. In this case, it informs the compiler this is a test JUnit is responsible for running.

Next, we declare our test's method with the line public void runPingPong_countUpToOne_ArrayList(). JUnit tests are actually methods! Also, they always start with public void.

The naming convention is: public void nameOfMethodWeAreTesting_descriptionOfBehavior_expectedReturnValue(). You can read more about this here.

We called our test method runPingPong_countUpToOne_ArrayList() because runPingPong() will be the name of our method. countUpToOne is the behavior this spec is testing. And ArrayList() is the data type we're expecting runPingPong() to return.

Note on Return Types

But why use an ArrayList for a single number? We do not want to modify previous specs when implementing future specs. Even though this behavior only requires a single number, our program will eventually return an entire ArrayList like this: [1, 2, "ping"]. If we wrote our test to anticipate an Integer instead of an ArrayList we would later have to go back and change this test when we implement behaviors to count higher than one.

JUnit Assertions

We've declared our test method. Now, let's fill it with code:

src/test/java/PingPongTest.java
import org.junit.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;

public class PingPongTest {

  @Test
  public void runPingPong_countUpToOne_ArrayList() {
    PingPong testPingPong = new PingPong();
    ArrayList<Object> expectedOutput = new ArrayList<Object>();
    expectedOutput.add(1);
    assertEquals(expectedOutput, testPingPong.runPingPong(1)); //I may be red..

  }

}

It's OK for the moment that the runPingPong() method is underlined red.

In the code above, we've done the following:

  • First, we create a new instance of our PingPong class called testPingPong.
  • Then, we create an ArrayList of the <Object> generic called expectedOutput, and add 1 to it (This is only included because our method returns an ArrayList, and this is how an ArrayList is constructed. If our method returned something else, like a String, we wouldn't need to construct it piece-by-piece with add()).
  • We import the necessary List and ArrayList packages at the top of the file.
  • Then, we write our first assertion using assertEquals(). This tells JUnit to confirm (or assert) whether the two arguments provided are equivalent.
    • The first argument, [1] represents the return value we expect. This is similar to the "Example Output" we included with our plain English specs in Intro to Programming.
    • The second argument, testPingPong.runPingPong(1), is a trial in which we call our runPingPong() method, providing it 1 as an argument.

Essentially, translated into "plain English" this test would read: "JUnit, test out the runPingPong() method. Give it the number 1 and make sure it correctly returns [1]."

By writing our first coded test, we've completed the second step in the Red, Green, Refactor workflow!

Running JUnit Tests

Let's run our test and see what happens! Simply right-click on the tab name (where it reads PingPongTest.java as a tab name in the editor) and select Run PingPongTest.

And we see:

Error:(15, 50) java: cannot find symbol
  symbol:   method runPingPong(int)
  location: variable testPingPong of type models.PingPong

Oh no, this isn't what a failing test looks like. This is a compiler error. Let's decipher it together. Since this is our very first test, we have not touched a line of program logic yet. However, the test command first compiles our source code, then tests the compiled code using JUnit. If the application cannot compile successfully, JUnit tests never even get a chance to run! Therefore, we have to make sure to address all compiler errors first.

In our case, the compiler is throwing an error because we have not written a method called runPingPong in our PingPong class being referenced in our test.

In our PingPong.java file, let's declare the runPingPong() method. But we will not add any logic until we see the test fail. We'll only declare them in order to avoid compiler errors:

src/main/java/PingPong.java
import java.util.ArrayList;
import java.util.List;

public class PingPong {

  public ArrayList<Object> runPingPong(int countUpTo){

  }
}
  • Here, we declare the method's return type as ArrayList because we know the return value will differ in length depending on what number the user enters (we cannot use an Array because they are unable to fluctuate in size).

  • We also include the generic <Object> because we know a ping pong application will eventually replace some numbers with Strings, and ArrayList<Object> can contain multiple differing data types.

  • We also import the ArrayList package at the top of the file if we haven't done so already.

Now, if we run the test again, we should see something like

/Users/epicodus-student/Desktop/java/ping-pong/src/main/java/PingPong.java:7: error: missing return statement

Alright. A different error this time! This one reads error: missing return statement. This is because our runPingPong() method isn't actually returning anything.

Again, running a test compiles our project, then runs tests using the compiled logic. Therefore, to begin running tests we need to make sure the project can actually compile successfully.

At this point, you may be asking "But I thought you told us not to write code before our test fails?!".

And that's true; we shouldn't write any program logic. We do, however, need to declare any methods and classes our tests reference. If we don't, the project will not successfully compile. Instead, the compiler will throw an error. If we can't successfully compile, we cannot run tests. If we cannot run tests, we cannot confirm the test fails appropriately.

So, to move past the missing return statement error, we need to add a return statement to our runPingPong() method. We can just returnnull for now.

ping-pong/src/main/java/PingPong.java
import java.util.ArrayList;
import java.util.List;

public class PingPong {

  public ArrayList<Object> runPingPong(int countUpTo){
    ArrayList<Object> result = new ArrayList<Object>();
    return null;
  }

}

It's important to understand the difference between declaring classes and methods in order to compile successfully, and writing the actual program logic. You do not write any program logic before writing a test for that behavior, and confirming it fails appropriately.. But you do need to declare necessary elements to get beyond any compiler errors.

If we run our test once more, we should see the following:

intellij-assertion-error

Awesome. This is a test failure message!

Reading JUnit Results

Notice that this screen indicates how many tests have been run, and how many have failed on the left. Additionally, it tells us which test failed in the line `PingPongTest > runPingPongcountUpToOneArrayList(PingPongTest.java:15). That's how we know the project has compiled successfully and tests were able to run.

It shows us what results we received from our test, too:

java.lang.AssertionError:
Expected :[1]
Actual   :null

This means the test failed because we designated an expected result of [1], but the actual result was null. This is useful - now we know why it failed. This helps us understand what we need to change to get our test to pass.

At first, it might seem like a failing test is bad; but remember, this is actually good news! We want our test to fail appropriately before we begin adding code. Only then we can know that we were successful when the test passes.

Now that we have a general understanding of how a JUnit test works, how to run them, and where to view their results, we'll continue following the Red, Green, Refactor workflow to build our Ping Pong application in the next lesson.


Example GitHub Repo for Ping Pong