On this page:
Overview
6.1 More Classes
Interlude – String is a class
6.2 From Examples to Testing
6.3 Summary
6.8

Lecture 6: Programming, Objectively

Related files:
  ExamplesBook.java  

Overview
6.1 More Classes

This example is from Ben Lerner http://www.ccs.neu.edu/course/cs2510/lecture3.html.

As we mentioned in the last section, there is a wide variety of data in the world that composes multiple simple values. We talked about Points in class, and Student records in the last set of notes. Let’s take another example, modeling an inventory of books. A simple representation of books would need to cover at least their title, author, and price. That’s straightforward to describe as a class:

Do Now!

Translate the English description above into a class definition.

class Book {
String title;
String author;
int price;
 
Book(String title, String author, int price) {
this.title = title;
this.author = author;
this.price = price;
}
}

The type String is a natural starting point for title and author, and the price is certainly some kind of number. Again, we see that we write the field definitions for title, author, and price without any values (they are uninitialized field definitions), and we include the same shape of constructor we talked about already, that has one this.someName = someName line for each field/parameter.

Let’s write some methods for Book. Imagine the store has periodic sales, where everything is discounted at particular rates. Write a function salePrice that takes a number representing a percentage (e.g. between 0 and 100), and returns a number representing the price with that percentage subtracted.

Do Now!

Try it yourself before reading on.

Following the recipe, we’ll first specify the method header, which we’ll write inside the Book class:

/* @param percentage A percent (between 0 and 100) of the discount to subtract @return The price with the discount subtracted */
int salePrice(int percentage) {
 
}

We should also write some examples, both of Books and of the method’s use:

class ExamplesBook {
Book schemer = new Book("The Little Schemer", "Daniel P. Friedman", 40);
Book stick = new Book("Make it Stick: The Science of Succesful Learning", "Peter C. Brown", 13);
Book pLaw = new Book("Parkinson's Law", "C. Northcote Parkinson", 30);
 
int sale1 = this.schemer.salePrice(25); // should be 30 int sale2 = this.stick.salePrice(50); // should be 7 int sale3 = this.pLaw.salePrice(10); // should be 27 }

This gives us a pretty good sense of how we’ll get to the result; we’ll have a subtraction and a multipication. Since we’re using ints, we need to be a bit careful about the order of operations:

/* @param percentage A percent (between 0 and 100) of the discount to subtract @return The price with the discount subtracted */
int salePrice(int percentage) {
return this.price - (this.price * percentage) / 100;
}

We can save this all in a file called ExamplesBook.java, run it, and confirm for ourselves that it runs as we expect. There are a number of ways we can extend this class. We might try to add more authors (the first two examples above actuall have multiple authors). We might add a field called subtitle, since the full title of "Make it Stick..." is a bit long. We could also add some new methods. We could ask, for instance, if two books have the same author. That would be a method on Book that needs to take in another Book reference, and compare the author fields somehow. Let’s work through this next.

Do Now!

What should we make sure to add when writing examples for this method?

/* @param other The Book to compare against @return true if this book and other have the same author, false otherwise */
boolean sameAuthor(Book other) {
 
}
 
class ExamplesBook {
 
// (omitting the examples from before, imagine it's here)  
Book reason = new Book("The Reasoned Schemer", "Daniel P. Friedman", 38);
 
boolean same1 = this.reason.sameAuthor(this.schemer); // should be true boolean same2 = this.reason.sameAuthor(this.stick); // should be false boolean same3 = this.schemer.sameAuthor(this.reason); // should be true  
}

Actually, for this specific example, it will work. But for more sophisticated uses of Strings, it won’t, so we’ll tackle the general solution now.

Now we just need to write the body of the method. We need to compare the author field, which is a String, of this book and the other book. Since + worked on both numbers and Strings, it’s naturaly to wonder if another operator, like ==, will work on both. It turns out that it won’t, for a very specific reason.

Interlude – String is a class

String is actually a class, and the values we’ve been using as Strings are actually objects! The == won’t work the way we expect here on objects (we’ll talk more about why later). This explains why we write String with a capital S – the convention for class names is that they start with a capital letter (as opposed to simple, or primitive, types like int and boolean).

It also helps explain how we can go about performing operations other than + on Strings. Since they are objects, we ought to be able to call methods on them! The String class defines a method .equals which can be used to compare one string to another to check if they have the same contents. That gives us what we need to finish off the method body of sameAuthor:

boolean sameAuthor(Book other) {
return this.author.equals(other.author);
}

If we run this, we get the answer we expect:

new ExamplesBook:1(

 this.schemer =

  new Book:2(

   this.title =  "The Little Schemer"

   this.author =  "Daniel P. Friedman"

   this.price = 40)

 this.stick =

  new Book:3(

   this.title =  "Make it Stick: The Science of Succesful Learning"

   this.author =  "Peter C. Brown"

   this.price = 13)

 this.pLaw =

  new Book:4(

   this.title =  "Parkinson's Law"

   this.author =  "C. Northcote Parkinson"

   this.price = 30)

 this.reason =

  new Book:5(

   this.title =  "The Reasoned Schemer"

   this.author =  "Daniel P. Friedman"

   this.price = 38)

 this.same1 = true

 this.same2 = false

 this.same3 = true)

There are a few things worth noting about our new realization that String is a class, and String values are objects:

To introduce another few String methods and practice with them, let’s write one more function. Sometimes when printing an inventory, we might have long titles, like the full title of "Make it Stick", that don’t fit in a column width-wise. A common trick is to truncate the string down to a certain length, and replace the end with "...". If the string is below the given length, we leave it alone. So let’s write truncateTitle, which will take a length cut the title down to, and return the truncated title.

/* @param length The number of characters to appear before "..." @return A new string containing length characters followed by "..." if the string was too long, or the original string otherwise */
String truncateTitle(int length) {
 
}
 
 
class ExamplesBook {
// examples from above omitted here  
String truncate1 = this.stick.truncateTitle(15); // Should be "Make it Stick: ..." String truncate2 = this.schemer.truncateTitle(20); // Should be "The Little Schemer" String truncate3 = this.schemer.truncateTitle(0); // Should be "..." }

We’ll need two String methods to do this:

Here are some examples:

class ExamplesBook {
// examples from above omitted here  
String ss1 = "abcd".substring(0, 1); // Produces "a" String ss2 = "abcd".substring(0, 4); // Produces "abcd" String ss3 = "Hello there".substring(0, 7); // Produces "Hello t"  
String l1 = "abcd".length(); // Produces 4 String l2 = "".length(); // Produces 0 String l3 = "The Little Schemer".length(); // Produces 18 }

By putting these together, we can write the body of truncate:

String truncateTitle(int length) {
if(this.title.length() > length) {
return this.title.substring(0, length) + "...";
}
else {
return this.title;
}
}

We put a few pieces together to make this happen. First, the description of the problem was conditional – we were to give one answer if the string was too long, and another if it was short enough. Second, we used length on the title field to compare it to the parameter to check which case needed to be processed. Third, we used the substring method to pick out the part of the string from the beginning (position 0), up to the specified length.

We often call the positions in a String the indices in the string. So the first character is at index 0.

Do Now!

What is the index of the last character, in terms of the string’s length?

6.2 From Examples to Testing

Let’s take stock of the whole program we’ve written so far in these notes:

class Book {
String title;
String author;
int price;
 
Book(String title, String author, int price) {
this.title = title;
this.author = author;
this.price = price;
}
 
/* @param percentage A percent (between 0 and 100) of the discount to subtract @return The price with the discount subtracted */
int salePrice(int percentage) {
return this.price - (this.price * percentage) / 100;
}
 
/* @param other The Book to compare against @return true if this book and other have the same author, false otherwise */
boolean sameAuthor(Book other) {
return this.author.equals(other.author);
}
 
/* @param length The number of characters to appear before "..." @return A new string containing length characters followed by "..." if the string was too long, or the original string otherwise */
String truncateTitle(int length) {
if(this.title.length() > length) {
return this.title.substring(0, length) + "...";
}
else {
return this.title;
}
}
 
}
 
 
class ExamplesBook {
Book schemer = new Book("The Little Schemer", "Daniel P. Friedman", 40);
Book stick = new Book("Make it Stick: The Science of Succesful Learning", "Peter C. Brown", 13);
Book pLaw = new Book("Parkinson's Law", "C. Northcote Parkinson", 30);
Book reason = new Book("The Reasoned Schemer", "Daniel P. Friedman", 38);
 
int sale1 = this.schemer.salePrice(25); // should be 30 int sale2 = this.stick.salePrice(50); // should be 7 int sale3 = this.pLaw.salePrice(10); // should be 27  
String truncate1 = this.stick.truncateTitle(15); // Should be "Make it Stick: ..." String truncate2 = this.schemer.truncateTitle(20); // Should be "The Little Schemer" String truncate3 = this.schemer.truncateTitle(0); // Should be "..."  
boolean same1 = this.reason.sameAuthor(this.schemer); // should be true boolean same2 = this.reason.sameAuthor(this.stick); // should be false boolean same3 = this.schemer.sameAuthor(this.reason); // should be true  
}

Here’s what we get when we run it:

⤇ ./run ExamplesBook

Tester Prima v.2.1

-----------------------------------

Tests defined in the class: ExamplesBook:

---------------------------

ExamplesBook:

---------------

 

 new ExamplesBook:1(

  this.schemer =

   new Book:2(

    this.title =  "The Little Schemer"

    this.author =  "Daniel P. Friedman"

    this.price = 40)

  this.stick =

   new Book:3(

    this.title =  "Make it Stick: The Science of Succesful Learning"

    this.author =  "Peter C. Brown"

    this.price = 13)

  this.pLaw =

   new Book:4(

    this.title =  "Parkinson's Law"

    this.author =  "C. Northcote Parkinson"

    this.price = 30)

  this.reason =

   new Book:5(

    this.title =  "The Reasoned Schemer"

    this.author =  "Daniel P. Friedman"

    this.price = 38)

  this.sale1 = 30

  this.sale2 = 7

  this.sale3 = 27

  this.truncate1 =  "Make it Stick: ..."

  this.truncate2 =  "The Little Schemer"

  this.truncate3 =  "..."

  this.same1 = true

  this.same2 = false

  this.same3 = true)

---------------

No test methods found.

Let’s observe a few things about this.

Before we handle that last point, there’s one step we could take to make it easier to double-check our work right away. Instead of putting the answer to the method call directly into the example, we could use == (in the case of int and boolean), or .equals() (in the case of Strings) to check if the answer is what we expect. Then we would just need to make sure that all the examples evaluate to true, which is easier than comparing them with comments.

Specifically, what if we write the examples as:

boolean sale1 = this.schemer.salePrice(25) == 30;
boolean sale2 = this.stick.salePrice(50) == 7;
boolean sale3 = this.pLaw.salePrice(10) == 27;
 
boolean truncate1 = this.stick.truncateTitle(15).equals("Make it Stick: ...");
boolean truncate2 = this.schemer.truncateTitle(20).equals("The Little Schemer");
boolean truncate3 = this.schemer.truncateTitle(0).equals("...");
 
boolean same1 = this.reason.sameAuthor(this.schemer) == true;
boolean same2 = this.reason.sameAuthor(this.stick) == false;
boolean same3 = this.schemer.sameAuthor(this.reason) == true;
 

Then we get this output for the examples portion:

this.sale1 = true

this.sale2 = true

this.sale3 = true

this.truncate1 = true

this.truncate2 = true

this.truncate3 = true

this.same1 = true

this.same2 = true

this.same3 = true)

That’s much easier to check visually – they’re all true! However, it’s much more frustrating if we make a mistake. Say we made an error and truncateTitle somehow produced "Make it Stick:...." instead of "Make it Stick: ...". The output would just show false, rather than the actual output of the method for us to compare and understand our mistake.

Professional testing systems do much more than just check equality and print results, but it is always a core feature they support.

Ideally, we’d like a system that can check for equality, summarize successes, and show us the reason for failures. Systems that do this are known as testing frameworks or testing libraries, and one comes built in to the libraries we’re using for the course.

A detailed description of the tester library is at http://www.ccs.neu.edu/course/cs2510/tester-doc.html.

The way testing works in this course is that we can write any number of methods starting with the word test that have the return type boolean and take a reference to an object of type Tester as a parameter. The Tester object has a particular method that we’ll use extensively called checkExpect, which takes any two values of the same type, compares them for equality, and prints useful messages if they aren’t equal.

It’s useful to write this all out, and describe the pieces. Here’s a method we could add to replace the first example we wrote, sale1:

// Originally we wrote this: int sale1 = this.schemer.salePrice(25); // Should be 30  
// We can write this instead: boolean testSale(Tester t) {
return t.checkExpect(this.schemer.salePrice(25), 30);
}

We also have to tell Java that we want to use the Tester class, since it isn’t built-in to Java (it’s part of a library we’re using). So this line is necessary at the top of the file whenever we mention Tester:

import tester.Tester;

If we run this version of the program, we get this new output at the end:

---------------

 

Ran 1 test.

All tests passed.

 

--- END OF TEST RESULTS ---

This is the testing library telling us that it ran the test method, and the expected values were equal to one another. We can add more tests in two ways. First, we can use multiple calls to checkExpect together in the test method, and combine them together with &&:

// We can write this instead of all the sale examples: boolean testSale(Tester t) {
return t.checkExpect(this.schemer.salePrice(25), 30) &&
t.checkExpect(this.stick.salePrice(50), 7) &&
t.checkExpect(this.pLaw.salePrice(10), 27);
}

Note that each call to checkExpect counts as one “test” in the output.

---------------

 

Ran 3 tests.

All tests passed.

 

--- END OF TEST RESULTS ---

Second, we can define as many test methods as we like. So we could define another that captures all the truncate tests, and another that captures all the same author tests:

boolean testSale(Tester t) {
return t.checkExpect(this.schemer.salePrice(25), 30) &&
t.checkExpect(this.stick.salePrice(50), 7) &&
t.checkExpect(this.pLaw.salePrice(10), 27);
}
 
boolean testTruncateTitle(Tester t) {
return t.checkExpect(this.stick.truncateTitle(15), "Make it Stick: ...") &&
t.checkExpect(this.schemer.truncateTitle(20), "The Little Schemer") &&
t.checkExpect(this.schemer.truncateTitle(0), "...");
}
 
boolean testSameAuthor(Tester t) {
return t.checkExpect(this.reason.sameAuthor(this.schemer), true) &&
t.checkExpect(this.reason.sameAuthor(this.stick), false) &&
t.checkExpect(this.schemer.sameAuthor(this.reason), true);
}

Now this produces:

---------------

 

Ran 9 tests.

All tests passed.

 

--- END OF TEST RESULTS ---

This nicely summarizes all the successes; in addition, the testing library is smart enough to correctly compare Strings with .equals and ints with ==, so we don’t have to worry about that detail. In addition, if we get something wrong, it’s quite helpful in pointing out how. For example, say we accidentally used (percentage / 100) (which rounds to 0) in the implementation of salePrice. Here’s the output we would get:

---------------

 

Ran 7 tests.

1 test failed.

 

Failed test results:

--------------

 

Error in test number 1

 

tester.ErrorReport: Error trace:

        at ExamplesBook.testSale(ExamplesBook.java:54)

        at java.util.concurrent.FutureTask.run(FutureTask.java:266)

actual:...........................................expected:

 

40................................................30

 

 

--- END OF TEST RESULTS ---

This prints the result of calling the method on the left, and on the right, the result we originally wrote down as the expected answer in the test. This helps us quickly see which test has failed.

It’s also important to note the counts of tests that ran – the message says that 7 tests ran and 1 failed (so 6 of them passed), but we wrote 9 tests! That’s because a test method stops when the first test within it fails. In this case, the first check inside testSale failed, so the other two didn’t run. That accounts for the difference. Often, this is useful because it makes the test output not be overwhelming and report all the different ways things are failing, since often multiple tests are failing for the same reason. If we really want tests to run even when others fail, we can always put them into separate test methods.

6.3 Summary