The Student class represents an individual student record. The Student class is a “plain old java object” (POJO) consisting mostly of getters and setters. The setters do have some complexity.
In this section you will use Test Driven Development (TDD): write tests first based on the requirements and design, run those tests (expecting them to fail because the code isn’t implemented yet), then implement the code until the tests pass. This cycle — test, implement, verify — helps ensure your implementation meets the specification before you move on.
Student State
A Student knows their first name (String), last name (String), id (String), email (String), password (String), and max credits (int). Create the fields for Student. All fields should be private.
Additionally, Student has a constant MAX_CREDITS that represents the maximum possible credits any student can have. Since the constant is useful in test cases, MAX_CREDITS is public and should be set to the value of 18. The constant MAX_CREDITS represents the max number of credits that any student in the system may have. The field maxCredits is the max credits that a specific student may enroll in.
Note that the Student class does not hash the password. We expect that the password is passed in as a hashed value. The reason for this is that the password should be hashed as soon as possible after the user enters it. That means the class right behind the GUI, StudentDirectory, handles the hashing.
Add the fields to Student now so that your test class will compile.
Create a StudentTest Class
If StudentTest doesn’t already exist, create it:
- Right click on
Studentand select New > JUnit Test Case. - Change the Source folder to
/PackScheduler/test. Click Next. - Select the
Studentconstructors, all public setters,hashCode(),equals(), andtoString(). Click Finish. - A new class
StudentTestwill be created in theedu.ncsu.csc216.pack_scheduler.userpackage in thetest/source folder.
If the StudentTest file isn’t in the right package or source folder, move it to the appropriate location. If it’s not in the right place, your tests may not be executed on Jenkins!
Since the teaching staff tests use hashed passwords, we are going to work with a hashPW field in StudentTest. We create the hashPW field in a static code block. Because the code block is static, it’s executed when the test class is instantiated by the JUnit runner. Note that for the moment, we are storing a hardcoded password of “password”. Storing plain text passwords in source code is a security vulnerability. Never include actual passwords in your source code. For our tests, we are currently using a default password of “password”. We’ll fix this problem in a later lab.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/** Test Student's first name. */
private String firstName = "first";
/** Test Student's last name */
private String lastName = "last";
/** Test Student's id */
private String id = "flast";
/** Test Student's email */
private String email = "first_last@ncsu.edu";
/** Test Student's hashed password */
private String hashPW;
/** Hashing algorithm */
private static final String HASH_ALGORITHM = "SHA-256";
//This is a block of code that is executed when the StudentTest object is
//created by JUnit. Since we only need to generate the hashed version
//of the plaintext password once, we want to create it as the StudentTest object is
//constructed. By automating the hash of the plaintext password, we are
//not tied to a specific hash implementation. We can change the algorithm
//easily.
{
try {
String plaintextPW = "password";
MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);
digest.update(plaintextPW.getBytes());
this.hashPW = Base64.getEncoder().encodeToString(digest.digest());
} catch (NoSuchAlgorithmException e) {
fail("An unexpected NoSuchAlgorithmException was thrown.");
}
}
Use hashPW wherever a password value is needed in your tests.
There are several resources provided for writing tests, including sample test code:
- Testing Packet
- Dr. Heckman’s Testing Lecture Notes
- Seasons Test Example
- Dr. Heckman’s Coverage and Static Analysis Lecture Notes
- Guided Project 1 CourseTest.java
Note that the tests for Student are similar to the tests for Course in Guided Project 1. You may use those tests as examples and reference!
Testing Strategies
When testing POJOs, you want to ensure that:
- Objects can be constructed correctly with valid inputs and that all fields are set as expected
- Invalid inputs to constructors and setters cause the appropriate
IllegalArgumentException - State changes through setters are handled correctly and invalid changes do not change the state
- Two objects are equal on the correct attributes (
equals()andhashCode()) - The
toString()method returns the correct comma-separated list of fields for writing to an output file
Use our standard strategies of test the requirements, equivalence class partitioning, boundary value analysis, and basis set testing of method control flow to develop test cases.
Your goal is to achieve at least 80% statement coverage by writing high quality tests that exercise most of the paths in your Student class. Note: Line/statement coverage is the primary metric used for grading. There is 1 point of extra credit for exceeding 90% statement coverage, an additional point of extra credit for obtaining 100% statement coverage, and a third point of extra credit for achieving 100% condition/branch coverage!
Make sure you run your tests frequently! If you find a bug in your solution, fix it!
Running Tests for Coverage
Before you start writing tests, make sure that you can run tests instrumented for coverage. Right click on the test/ source folder and select Coverage As > JUnit Test.
When evaluating coverage locally, focus only on the coverage of Student, StudentRecordIO, and StudentDirectory. The coverage of the test classes themselves and any user interface classes are not considered. Jenkins is set up to exclude test classes and user interface classes, but your local EclEmma is not. Don’t panic if the overall numbers seem low.
One last thing to check is that EclEmma is reporting the right metrics. Click the triple dots or down arrow (View Menu) option in the Coverage view. Select Line Counters for seeing statement/line coverage. If you want to see condition coverage (for extra credit!), use the Branch Counters option.
Test and Implement Student Constructors
Write Tests for Constructors
Student has two constructors. Write your tests in StudentTest before implementing the constructors. The tests will not pass (and may not compile) until you implement the constructors and all of the setter method they will call — that is expected in TDD.
Student(String firstName, String lastName, String id, String email, String password, int maxCredits): calls the setters for each of the fields. If the setters throw an IllegalArgumentException, the exception should pass through to the client — the constructor does NOT catch the exception.
- Test that you can construct a valid
Studentand verify all fields are set correctly using getters. If you want to assert on all the state at once, see the examples in the tests forCoursefrom GP1.
1
assertEquals("first", s1.getFirstName());
- Test that each invalid parameter causes an
IllegalArgumentException— test one invalid parameter at a time, with all other parameters valid:
1
2
3
Exception e1 = assertThrows(IllegalArgumentException.class,
() -> new Student(null, "last", "id", "email@ncsu.edu", "hashedpw", 15));
assertEquals("Invalid first name", e1.getMessage());
Student(String firstName, String lastName, String id, String email, String password): calls the other constructor with the default max credits value of 18.
- Test that after calling this constructor,
maxCreditsis always 18.
Run your tests now. They may not compile yet since the constructors are not implemented — that is the TDD red phase!
Implement Student Constructors
Implement the two constructors. Each constructor calls the setters; if a setter throws an IllegalArgumentException, the exception passes through the constructor — do NOT catch the exception. Just call the setters!
Run your tests. Fix any implementation issues until all constructor tests pass.
Implement Student Getters
All getters for Student fields are straightforward; they return the field. Use Source Code Generation to create the getters. You will get coverage of the getters through your constructor and setter tests.
Test and Implement Student Setters
Write Tests for Setters
The setters are more complex because they check to make sure that the Student fields are not invalid as defined in the requirements and Use Case 5: Add Student to Student Directory. If a value is invalid, the setter throws an IllegalArgumentException.
You may be wondering why you test the setters after testing them through the Student constructors. The reason is that while you may have achieved coverage through the constructors, you haven’t tested that setters work correctly after an object is constructed. The setter tests also confirm that invalid input does not change the existing state.
The strategy for testing each setter:
- Test that the setter correctly changes a valid value and that the change is visible through a getter.
- Test that the setter throws an
IllegalArgumentExceptionfor invalid input without changing the state:
1
2
3
4
5
Student s = new Student("first", "last", "id", "email@ncsu.edu", "hashedpassword");
Exception e1 = assertThrows(IllegalArgumentException.class,
() -> s.setFirstName(null));
assertEquals("Invalid first name", e1.getMessage());
assertEquals("first", s.getFirstName()); // State should not have changed
Test each of the following setters with both valid and invalid inputs:
setFirstName(String firstName): throws anIllegalArgumentExceptionif the parameter is null or an empty string.setLastName(String lastName): throws anIllegalArgumentExceptionif the parameter is null or an empty string.setId(String id): throws anIllegalArgumentExceptionif the parameter is null or an empty string. Note that in the design,setId()is listed as aprivatemethod — aStudent’s id shouldn’t change after creation. That means you can only testsetId()through theStudent()constructor. If you’ve already coveredsetId()in your constructor tests, you don’t need a separate test method for it.setEmail(String email): throws anIllegalArgumentExceptionif:- the parameter is null or an empty string
- email doesn’t contain an ‘@’ character
- email doesn’t contain a ‘.’ character
- the index of the last ‘.’ character in the email string is earlier than the index of the first ‘@’ character (e.g.,
first.last@addresswould be invalid)
setPassword(String password): throws anIllegalArgumentExceptionif the parameter is null or an empty string.setMaxCredits(int maxCredits): throws anIllegalArgumentExceptionif the parameter is less than 3 or greater than 18.
Run your tests now — they will fail or not compile because the setters are not implemented yet.
Implement Student Setters
Implement the setters. The Alternative Flows of Use Case 5: Add Student to Student Directory describe the string messages that should be used when constructing the IllegalArgumentException for each invalid case.
Don’t forget to make setId() private to meet the design!
Run your tests. Fix any implementation issues until all setter tests pass.
Test and Implement Student hashCode() and equals()
Write Tests for hashCode() and equals()
Create three or more Student objects. Two of the objects have the exact same state and the rest have at least one piece of different state.
- For
equals(): test that two objects with the same state are equal, and that two objects with different state are not equal. - For
hashCode(): if two objects are equal, they must have the same hashcode.
Use the Guided Project 1 CourseTest.testEquals() and CourseTest.testHashCode() methods as guides for writing your tests.
Implement hashCode() and equals()
Generate hashCode() and equals() on all of the Student fields.
Note that achieving 100% statement or condition coverage for the Eclipse-generated equals() and hashCode() methods is impossible with the null checks on Student fields during construction (since null fields are rejected at construction). You may modify the equals() and hashCode() methods to remove null checks to achieve 100% statement and condition coverage for extra credit!
Run your tests to verify.
Test and Implement Student toString()
Write a Test for toString()
toString() should return a comma-separated string of the student’s fields as defined in the Student Records Data Format:
1
firstName,lastName,id,email,hashedPassword,maxCredits
For example: first,last,flast,first_last@ncsu.edu,hashedpassword,18
Using the hashPW field set up in setUp(), write the test:
1
2
3
4
5
@Test
public void testToString() {
Student s1 = new Student("first", "last", "flast", "first_last@ncsu.edu", hashPW);
assertEquals("first,last,flast,first_last@ncsu.edu," + hashPW + ",18", s1.toString());
}
Implement toString()
Override toString() by right clicking in the editor (inside the Student class definition) and selecting Source > Override/Implement Methods. Select toString().
toString() should return the fields as a comma-separated string:
1
firstName,lastName,id,email,hashedPassword,maxCredits
Run your tests to verify.
Run Tests for Coverage
Run your tests instrumented for coverage. Make sure that your tests execute at least 80% of the statements in Student. Remember there is extra credit for more coverage!
Javadoc your Code
Make sure that you Javadoc the Student class, state, and methods. For the overridden methods equals(), hashCode(), and toString(), remove the green comments and Javadoc them to describe how the methods work in Student. Do NOT delete the @Override annotation.
Also Javadoc the StudentTest class and methods. We DO expect that all test classes are commented!
Run CheckStyle to ensure that your Javadoc has all elements.
GitHub Resources:
Push to GitHub
Push your PackScheduler project to GitHub
- Add the unstaged changes to the index.
- Commit and push changes. Remember to use a meaningful commit message describing how you have changed the code.
Check the following items on Jenkins for your last build and use the results to estimate your grade:
Check Jenkins
At this point your project should build on Jenkins with a Yellow ball. That is because there are failing tests in StudentRecordIO that we haven’t gotten to yet. When fixing test failures, focus on failures in TS_StudentTest. Make sure that all TS_StudentTest methods are passing before moving on to StudentRecordIO.
