Skip to content

Lab 4: Car Class and Test

In this lab, you will implement a Car class and corresponding CarTest. You will learn about non-static attributes and methods. And you will gain more experience with JUnit and code coverage.

Learning Objectives

After completing this lab, you should be able to:

  • Interpret a UML class diagram and stub out the corresponding code.
  • Write JUnit tests that create objects and assert non-static methods.

Step 1. Getting Started

Do the following steps in VS Code:

  1. Create a lab04 folder under your src/labs folder.
  2. Add two new files: Car.java and CarTest.java.
  3. Write the package and public class statements.
  4. Write a Javadoc comment with your name and date.

When finished, your Car.java and CarTest.java should look like this:

VS Code screenshot with Car.java

Step 2. UML Class Diagram

Based on the diagram below, declare each attribute and write a stub for each method. Do not implement any methods yet, but make the code compile. For example, if a method returns a String, write return ""; as a stub (placeholder). Show your code to the instructor or TA before proceeding to the next step.

classDiagram
    class Car {
        - make: String
        - speed: double
        - year: int
        + Car(make: String, year: int)
        + getMake() String
        + getSpeed() double
        + getYear() int
        + accelerate() String
        + brake()
        + equals(obj: Object) boolean
        + toString() String
    }

Note

The brake() method is void (does not return a value). The word void is usually omitted in UML class diagrams.

Step 3. Constructor, Getters

The purpose of a constructor is to initialize the attributes of this object. Notice the Car class has three attributes: make, speed, and year. The constructor has two parameters: make and year. Use the parameters to initialize this object's make and year. Initialize this object's speed to zero. Then write the Javadoc comment for the constructor.

Note

When writing documentation for constructors, do not use the word create. Constructors do not "create" objects; the new operator creates an object. Once an object has been created, the constructor is called to initialize the object.

The purpose of a getter method is to return the current value of an attribute. Notice that the attributes are private, meaning they can be accessed only in the Car class. Other classes, like CarTest, will need to use the public getters to access the attributes. Implement each getter by returning the corresponding attribute.

Javadoc comments are not required for most getter methods. However, a Javadoc comment is required if a getter does more than simply return an attribute.

Step 4. Test the Constructor

Before implementing the other methods in Car.java, let's make sure the constructor and getter methods work correctly. Open CarTest.java and add the following imports to the top:

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

Then add the following method:

@Test
public void testConstructor() {
}

Note

The @Test annotation tells JUnit that the following method is a test method. Test classes sometimes have other methods, such as helper methods, that are not run as tests.

Add the following code to testConstructor():

  1. Create two Car objects.
    • Each car should have a different make and year.
  2. Write three assertEquals statements for each car:
    • getMake() should return the make you chose.
    • getSpeed() should return zero (at this point).
    • getYear() should return the year you chose.

Run the test by clicking the green play () button next to the method. If the test fails, make whatever changes are necessary in your code. After the test passes, click the Watch button (bottom left) to show code coverage.

VS Code watch button

In Car.java, the constructor and getters should be highlighted in green, meaning those lines were executed during the test. The other methods should be highlighted in red, meaning they were not executed during the test. Click the Watch button again to hide code coverage.

Step 5. Accelerate and Brake

The accelerate() method increases the current speed of the car by 5 miles per hour. After increasing the speed, accelerate() returns a string describing the change. Use the format "15.0 + 5.0 = 20.0" where 15.0 is the old speed and 20.0 is the new speed.

Note

The String.format() method is useful for formatting double values. For example, you can format the speed values to one decimal place as follows:

return String.format("%.1f + 5.0 = %.1f", speed - 5, speed);

Your code must not allow the speed to increase above 150 miles per hour. If the speed is already 150, and accelerate() is called, the speed should remain 150. In that case, the method should return the string "150.0 + 0.0 = 150.0".

The brake() method should decrease the current speed by 5 miles per hour. Your code must not allow the speed to decrease below 0 miles per hour.

Write Javadoc comments for accelerate() and brake() before moving on to the next step.

Step 6. Test Driving the Car

Add the following method to CarTest:

@Test
public void testAcceleration() {
}

This test should do the following:

  1. Create a Car object.
  2. Call the accelerate() method.
    • Assert that the method returned "0.0 + 5.0 = 5.0".
    • Assert that the Car object's speed is now 5.
  3. Call the brake() method.
    • Assert that the Car object's speed is now 0.

Run the test and make sure the test passes. Click the Watch button to show code coverage. Some lines of Car.java are green, some are yellow, and some are red. You still need to test accelerating when speed is 150 and braking when speed is 0.

Add the following method to CarTest:

@Test
public void testSpeedLimits() {
}

This test should do the following:

  1. Create a Car object.
  2. Call the brake() method.
    • Assert that the Car object's speed is 0.
  3. Call the accelerate() method 30 times, and then:
    • Assert that the method returned "145.0 + 5.0 = 150.0".
    • Assert that the Car object's speed is now 150.
  4. Call the accelerate() method one more time.
    • Assert that the method returned "150.0 + 0.0 = 150.0".
    • Assert that the Car object's speed is still 150.

Step 7. The equals() method

The purpose of the equals() method is to determine if two objects are equivalent. Replace your equals() method with the following solution:

@Override
public boolean equals(Object obj) {
    if (obj instanceof Car) {
        Car car = (Car) obj;
        return car.make.equals(this.make) && car.year == this.year;
    }
    return false;
}

The above code:

  1. Overrides the default implementation of equals().
    • By default, equals() returns true if this and obj are the same object.
  2. Checks if obj, which can be any type of object, is actually a Car object.
    • If not, then equals() returns false at the end.
  3. Declares a Car variable to reference the same object as obj.
  4. Determines whether car has the same make and year as this object.

Step 8: The toString() method

The purpose of the toString() method is to return a string that represents the object. Replace your toString() method with the provided solution:

@Override
public String toString() {
    return String.format("A %d %s that is going %.1f mph",
        year, make, speed);
}

The above code:

  1. Overrides the default implementation of toString().
    • By default, toString() returns the class name and the memory location of the object. For example, "labs.lab04.Car@5b6f7412".
  2. Uses the String.format() method to build a string based on the object's attributes.

Note

Javadoc comments are not required for @Override methods, because the documentation is already defined by the default implementation. In VS Code, you can hover the mouse over a method's name to see the documentation.

Step 9. Final Test and Submit

Add the following test methods to CarTest:

@Test
public void testEquals() {
    Car car1 = new Car("Nissan", 2003);
    Car car2 = new Car("Nissan", 2003);
    Car car3 = new Car("Honda", 2003);
    Car car4 = new Car("Nissan", 2024);
    assertTrue(car1.equals(car2));
    assertFalse(car1.equals(car3));
    assertFalse(car1.equals(car4));
    assertFalse(car1.equals("car5"));
}

@Test
public void testString() {
    Car car = new Car("Nissan", 2003);
    assertEquals("A 2003 Nissan that is going 0.0 mph", car.toString());
}

Notice that testEquals() compares car1 with four different objects:

  1. car2 has the same make and year.
  2. car3 has a different make.
  3. car4 has a different year.
  4. "car5" (in quotes) is a string.

Calling the equals() method four times is necessary to cover all the branches. In contrast, the toString() method does not have any branches. So only one method call is necessary to get full coverage of toString().

As a final step, click the green play () button next to public class CarTest. All five tests should pass, and you should have 100% code coverage of the Car class.

Submit both Car.java and CarTest.java to Gradescope. Points will be allocated as follows:

Criterion Points Details
Compile 0 pts Success Required
CompileOfficialTests 0 pts Success Required
Style 0 pts Success Required
SelfTests 2 pts Partial Credit Possible
Coverage 3 pts Partial Credit Possible
OfficialTests 5 pts Partial Credit Possible