Your best friend -- Phil M. Buff -- has an extensive DVD collection. Phil's collection is so big, he's having trouble remembering which movies he owns.
The good news is that Phil already has a text file containing the names of the movies, their directors, and the year they were made. Phil has asked you to write a program that reads his text file, and then lets him:
To make a start at solving this problem, we will
Create a new project named lab1 in which to store the files related to this project. The first few entries in Phil's text file are as follows:
12 Angry Men 1957 Sidney Lumet 12 Monkeys 1995 Terry Gilliam 2001: A Space Odyssey 1968 Stanley Kubrick 2010: T. Year We Make Contact 1984 Peter Hyams T. 300 Spartans 1962 Rudolph Maté
The structure of this information (title, year, and director, each on separate lines; movies separated by a blank line) will determine the behavior of some of our methods.
From the structure of Phil's text file, our Movie class needs to store these attributes about a movie:
/* Movie.h
* Student Name:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin College.
*/
#ifndef MOVIE_H_
#define MOVIE_H_
#include <string>
using namespace std;
class Movie {
public:
Movie();
private:
string myTitle;
int myYear;
string myDirector;
};
#endif /*MOVIE_H_*/
Since a C++ class consists of two files -- a header file whose name ends in .h, and an implementation file whose name ends in .cpp -- we also need to create Movie.cpp:
/* Movie.cpp * Student Name: * Date: * Begun by: Joel Adams, for CS 112 at Calvin College. */ #include "Movie.h"
The Movie.cpp file is where we will define the methods of our Movie class. We don't have any methods to place there yet, but we will shortly...
Next -- before we build any methods -- we create a class to store the tests we will run on the methods of our Movie class. Create a new class named MovieTester that looks like this at the outset:
/* MovieTester.h
* Student Name:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin College.
*/
#ifndef MOVIETESTER_H_
#define MOVIETESTER_H_
#include "Movie.h"
#include <iostream>
using namespace std;
class MovieTester {
public:
void runTests() const;
};
#endif /*MOVIETESTER_H_*/
So far, our MovieTester class contains a prototype or declaration of a single method named runTests(). (We'll be adding other method prototypes shortly.) Generally speaking, C++ header (.h) files contain class and method/function declarations; function definitions are placed in the implementation (.cpp) files.
Since a C++ class consists of two files -- the header file and the implementation file -- we also need to create MovieTester.cpp:
/* MovieTester.cpp
* Student Name:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin College.
*/
#include "MovieTester.h"
void MovieTester::runTests() const {
cout << "Testing Movie class..." << endl;
cout << "All tests passed!" << endl;
}
Note that since we have no tests to run (yet), we just provide a "stub" definition for our runTests() method for now.
To actually run the tests, we create a source file named tester.cpp containing a simple main() function:
/* tester.cpp tests the classes in our project.
*
*/
#include "MovieTester.h"
#include <iostream>
using namespace std;
int main() {
MovieTester mt;
mt.runTests();
}
At this point, you should able to compile tester.cpp without errors, and run it to produce the messages:
Testing Movie class... All tests passed!
If you have errors, find and fix them before continuing.
Our Movie class's default constructor should initialize each instance variable to a default value. If we choose "empty" or zero values as our default values, we might build a test method in MovieTester.cpp to test our constructor as follows:
void MovieTester::testDefaultConstructor() const {
cout << "- Default Constructor ... " << flush;
Movie m;
assert( m.getTitle() == "" );
assert( m.getYear() == 0 );
assert( m.getDirector() == "" );
cout << " Passed!" << endl;
}
That is, we expect to be able to send a Movie messages like
getTitle(), getYear(), and so on, to access the values of its
instance variables, so we use those to test their values.
Next, we can add a prototype for this method to our MovieTester class:
class MovieTester {
public:
void runTests() const;
void testDefaultConstructor() const;
};
You will also need to #include any header files this method requires (e.g., <cassert>), and then invoke this method in our MovieTester::runTests() method (in MovieTester.cpp):
void MovieTester::runTests() const {
cout << "Testing Movie class..." << endl;
testDefaultConstructor();
cout << "All tests passed!" << endl;
}
Even after we do all of that, we still get the following error message:
../MovieTester.h: In member function `void MovieTester::testDefaultConstructor()': ../MovieTester.h:21: error: 'class Movie' has no member named 'getTitle' ../MovieTester.h:22: error: 'class Movie' has no member named 'getYear' ../MovieTester.h:23: error: 'class Movie' has no member named 'getDirector'
The problem is that our Movie class does not yet provide the methods to access its members. To eliminate these errors, our next step is to go back to Movie and add protoypes and definitions for the missing methods.
In Movie.h, we add the prototypes:
class Movie {
public:
Movie();
string getTitle() const;
int getYear() const;
string getDirector() const;
private:
string myTitle;
int myYear;
string myDirector;
};
And in Movie.cpp, we define these methods:
#include "Movie.h"
string Movie::getTitle() const {
return myTitle;
}
int Movie::getYear() const {
return myYear;
}
string Movie::getDirector() const {
return myDirector;
}
At this point, our project should compile without any errors.
However, it will generate a linking error:
/usr/bin/ld: Undefined symbols: Movie::Movie() collect2: ld returned 1 exit status make: *** [lab1] Error 1 make: Target `all' not remade because of errors.ld is the name of the linker, that links function and method calls to their definitions. Anytime you see ld in an error message, the compiler found a prototype for the function or method, but the linker was unable to find a definition for the indicated method -- in this case, the default constructor Movie() -- the method we are trying to test (and that we have not yet written). So let's write it!
#include "Movie.h"
Movie::Movie() {
myTitle = "";
myYear = 0;
myDirector = "";
}
...
Then we add a prototype for it to our Movie class in Movie.h:
class Movie {
public:
Movie();
string getTitle() const;
int getYear() const;
string getDirector() const;
private:
string myTitle;
int myYear;
string myDirector;
};
With that, all of our classes should compile without error. When we run them, we should see:
Testing class Movie... - Default Constructor ... Passed! All tests passed!Congratulations! You've just written your first C++ methods using test-driven development!
The key to test-driven development is sometimes called programming by intent -- you have to know what you intend a method to do, write a test-method that thoroughly tests your intent, and then write the simplest possible method that passes your test.
Test-driven development is the sort of thing that gets easier with practice, so we are starting with "simple" methods before we progress to more complicated ones.
As we will see, test-driven development also supports regression-testing -- making sure that changes you make to a class in one method do not cause another method to "break".
In addition to a default constructor, most classes should provide an explicit-value constructor that lets the programmer initialize its members to specific values. For example, a test-method for such a constructor might look like this:
void MovieTester::testExplicitConstructor() const {
cout << "- Explicit Constructor... " << flush;
Movie m("Bambi", 1942, "David Hand");
assert( m.getTitle() == "Bambi" );
assert( m.getYear() == 1942 );
assert( m.getDirector() == "David Hand" );
cout << "Passed!" << endl;
}
Define that method in MovieTester.cpp, then place a prototype for it method in your MovieTester class. Finally, invoke it in your runTests() method:
void MovieTester::runTests() const {
cout << "Testing class Movie..." << endl;
testDefaultConstructor();
testExplicitConstructor();
cout << "All tests passed!" << endl;
}
then the only compilation error we should see is because the compiler cannot (yet) find a prototype
(or definition) for our explicit-value constructor:
../MovieTester.h: In member function `void MovieTester::testExplicitConstructor() const': ../MovieTester.h:31: error: no matching function for call to 'Movie::Movie(const char [6], int, const char [11], int)' ../Movie.h:25: note: candidates are: Movie::Movie() ../Movie.h:11: note: Movie::Movie(const Movie&)That means it's time to define it!
Once again, we write the simplest possible method that satisfies our test. Place this definition in Movie.cpp:
Movie::Movie(const string& title, int year, const string& director) {
myTitle = title;
myYear = year;
myDirector = director;
}
Then place its prototype in our Movie class. Now
our test-program should compile and run correctly:
Testing class Movie... - Default Constructor... Passed! - Explicit Constructor... Passed! All tests passed!
To see what happens when you make a mistake, simulate making a mistake by "commenting out" one of the lines in our constructor:
Movie::Movie(const string& title, int year, const string& director) {
myTitle = title;
// myYear = year;
myDirector = director;
}
Now when you run your method, the explicit-value constructor test will fail:
Testing class Movie... - Default Constructor... Passed! - Explicit Constructor... ../MovieTester.h:34: failed assertion `m.getYear() == 1942'By checking whether or not your methods pass the tests, test-driven development provides a methodology to help you keep from making mistakes.
"Uncommment" the line in your constructor and make sure that your code passes the tests before continuing.
Gone with the Wind 1939 Victor Fleming Star Wars 1977 George Lucas
We can then define a method in MovieTester.cpp to test our input method, which we will call readFrom():
void MovieTester::testReadFrom() const {
cout << "- readFrom()... " << flush;
ifstream fin("movieTest.txt");
assert( fin.is_open() );
Movie m;
m.readFrom(fin);
assert( m.getTitle() == "Gone with the Wind" );
assert( m.getYear() == 1939 );
assert( m.getDirector() == "Victor Fleming" );
string blankLine;
getline(fin, blankLine);
m.readFrom(fin);
assert( m.getTitle() == "Star Wars" );
assert( m.getYear() == 1977 );
assert( m.getDirector() == "George Lucas" );
cout << "Passed!" << endl;
}
As before, we must (a) place a prototype for this method in our MovieTester class, (b) invoke it in MovieTester::runTests(), and (c) add any #include directives it needs (e.g., <fstream>). When the only error we see is the compiler being unable to find our readFrom() method, we are ready to define it!
Note that our test method invokes readFrom() multiple times. This is a good idea, as invoking a method multiple times will sometimes exhibit errors that do not show up with a single invocation of the method.
Now that we know what our input method has to do to pass the test, we write that method. We might try the following definition:
void Movie::readFrom(istream& in) {
getline(in, myTitle);
in >> myYear;
getline(in, myDirector);
}
Then add a prototype for this method to the Movie class.
Your program should compile correctly.
However when we try to run it, one of our assertions fails:
- Default Constructor... Passed! - Explicit Constructor... Passed! - readFrom()... ../MovieTester.h:47: failed assertion `m.getDirector() == "Victor Fleming"'The problem here is a subtle issue in our readFrom() method: The getline() method stops when it sees a newline or linefeed character, and it consumes the newline or linefeed. The >> operator we are using to read myYear stops when it sees any white space characters, including a newline or linefeed character, but it leaves such characters unconsumed in the istream. So the problem is that >> is leaving the newline unconsumed in the istream, and when the subsequent call to getline() begins reading, the first thing it encounters is that newline. As a result, myDirector is receiving the empty string as its value, causing our assertion to fail.
We thus need to "clean out" the newline that follows the year, so that our final call to getline() can read the director's name into myDirector. One way to "clean out" such end of line characters is to use the getline() function after we have read myYear:
void Movie::readFrom(istream& in) {
getline(in, myTitle);
in >> myYear;
string newLine; getline(in, newLine);
getline(in, myDirector);
}
Now, when we compile and run, we pass all our tests:
Testing class Movie... - Default Constructor... Passed! - Explicit Constructor... Passed! - readFrom()... Passed! All tests passed!
Once we have passed all our tests, we can continue to the next operation.
Testing such a method using an assert() takes a bit more effort:
void MovieTester::testWriteTo() const {
cout << "- writeTo()... " << flush;
// one movie
Movie m1("Raiders of the Lost Ark", 1981, "Steven Spielberg");
ofstream fout("writeTo.txt");
m1.writeTo(fout);
Movie m2;
fout.close();
ifstream fin("writeTo.txt");
m2.readFrom(fin);
assert( m2.getTitle() == "Raiders of the Lost Ark" );
assert( m2.getYear() == 1981 );
assert( m2.getDirector() == "Steven Spielberg" );
fin.close();
cout << " 1 " << flush;
// two movies
Movie m3("Blade Runner", 1982, "Ridley Scott");
fout.open("writeTo.txt");
m1.writeTo(fout); // write 1st movie
fout << "\n"; // write a blank line
m3.writeTo(fout); // write 2nd movie
fout.close();
fin.open("writeTo.txt");
Movie m4, m5;
m4.readFrom(fin); // read 1st movie
string blankline; getline(fin, blankline); // eat the blank line
m5.readFrom(fin); // read 2nd movie
fin.close();
assert( m4.getTitle() == "Raiders of the Lost Ark" );
assert( m4.getYear() == 1981 );
assert( m4.getDirector() == "Steven Spielberg" );
assert( m5.getTitle() == "Blade Runner" );
assert( m5.getYear() == 1982 );
assert( m5.getDirector() == "Ridley Scott" );
cout << " 2 " << flush;
cout << "Passed!" << endl;
}
Two things are worth noting about this test:
After placing a prototype in our MovieTester class and invoking this method in runTests(), the only error we should generate is due to writeTo() not being defined:
../MovieTester.h: In member function `void MovieTester::testWriteTo() const': ../MovieTester.h:66: error: 'class Movie' has no member named 'writeTo'So we are now ready to define that method!
void Movie::writeTo(ostream& out) const {
out << myTitle << '\n'
<< myYear << '\n'
<< myDirector << '\n';
}
Define this method, then place a prototype for it in your Movie class; then compile and run
the test-application. If all is well, you should pass all the tests.
Congratulations! You have built a reasonably complicated Movie class!
Our next class is the MovieCollection class, in which we will store the movies of our friend Phil's collection. To test this new class, we will create a CollectionTester class, similar to the MovieTester class we built previously. We might start with this declaration:
/* CollectionTester.h tests the MovieCollection class.
* Student Name:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin College.
*/
#ifndef COLLECTIONTESTER_
#define COLLECTIONTESTER_
class CollectionTester {
public:
void runTests() const;
};
#endif /*COLLECTIONTESTER_*/
and
/* CollectionTester.cpp defines the MovieCollection test-methods.
* Student Name:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin College.
*/
#include "CollectionTester.h"
void CollectionTester::runTests() const {
cout << "Testing class MovieCollection..." << endl;
cout << "All tests passed!" << endl;
}
This is sufficient that we can now create a CollectionTester object in our tester.cpp file's main() method, and send it the runTests() message:
#include "MovieTester.h"
#include "CollectionTester.h"
int main() {
MovieTester mt;
mt.runTests();
CollectionTester ct;
ct.runTests();
}
Note that we do not replace our MovieTester tests. We want to continue to continue to re-run the Movie tests each time we test our MovieCollection, to make sure that nothing we write "breaks" something that worked previously.
This kind of testing -- in which we rerun all of our tests every time we modify the system to ensure that our modifications haven't broken something -- is called regression testing. Regression testing is a powerful technique for writing reliable software.
At this point, you should be able to run your test-application and pass all of the tests.
If we think about our problem a bit, the DVD collection is stored in a file. It really doesn't make much sense to build a default-constructor (at least not yet) for our MovieCollection class. Instead, we will build an explicit constructor that takes the name of a data file as its parameter, and then loads the collection using the data from that file.
To test our constructor, we create a (small) test file named testCollection.txt, containing just a few movies. We can then write a test-method for our MovieCollection constructor:
void CollectionTester::testConstructor() const {
cout << "- constructor..." << flush;
MovieCollection mc("testCollection.txt");
cout << " Passed!" << endl;
}
invoke it in runTests():
void CollectionTester::runTests() const {
cout << "Testing class MovieCollection..." << endl;
testConstructor();
cout << "All tests passed!" << endl;
}
and place its prototype in our CollectionTester class:
class CollectionTester {
public:
void testConstructor() const;
void runTests() const;
};
The compiler will generate an error because we have not yet defined a MovieCollection class. That means it's time to do so!
A simple declaration for class MovieCollection is as follows:
#ifndef MOVIECOLLECTION_H_
#define MOVIECOLLECTION_H_
#include <string>
using namespace std;
class MovieCollection {
public:
MovieCollection(const string& fileName);
};
#endif /*MOVIECOLLECTION_H_*/
This should eliminate all compilation errors, but may leave a
linking error (unless your IDE has created a
MovieCollection stub for you).
While our class declares the MovieCollection constructor,
it does not yet defined that constructor.
Before we do so, let's complete the test-method for our constructor.
Our test-method needs to do something with mc -- to somehow determine whether or not it is getting initialized correctly. Since we want to be able to find the movies by specific directors, we might extend testDefaultConstructor() as follows:
void CollectionTester::testConstructor() const {
cout << "- constructor..." << flush;
MovieCollection mc("testCollection.txt");
// case of 1 movie
vector<Movie> v1 = mc.searchByDirector("Hand");
assert( v1.size() == 1 );
assert( v1[0].getTitle() == "Bambi" );
cout << " 1 " << flush;
// case of 2 movies
vector<Movie> v2 = mc.searchByDirector("Spielberg");
assert( v2.size() == 2 );
assert( v2[0].getTitle() == "Jaws" );
assert( v2[1].getTitle() == "Raiders of the Lost Ark" );
cout << " 2 " << flush;
// case of no movies
vector<Movie> v3 = mc.searchByDirector("Hitchcock");
assert( v3.size() == 0 );
cout << " 3 " << flush;
cout << " Passed!" << endl;
}
Note that a test-method should test all aspects of an operation -- in this case:
Note also that our test method assumes the existence of a searchByDirector() method, which will return all of the movies in our collection that were directed by a given director. To accomplish this, our method assumes the movies are returned in a vector -- an array-like C++ standard structure that allows us to access any of its individual movies via an index value. To pass this test, we must (a) define the constructor, and (b) define the searchByDirector() method.
Before we build the constructor, we have to decide how we are going to store the collection. One way we might do so is using the STL vector class-template. To do so, we can add a vector<Movie> instance variable to our MovieCollection class:
/* MovieCollection.h ...
* ...
*/
#include <string>
#include "Movie.h"
#include <vector>
#include <cassert>
using namespace std;
class MovieCollection {
public:
MovieCollection(const string& fileName);
private:
vector<Movie> myMovies;
};
Given this structure, our constructor can now do its job as follows:
/* MovieCollection.cpp
* ...
*/
#include "MovieCollection.h"
#include <fstream>
using namespace std;
MovieCollection::MovieCollection(const string& fileName) {
ifstream fin( fileName.c_str() );
assert( fin.is_open() );
Movie aMovie;
do {
aMovie.readFrom(fin);
string blankLine;
getline(fin, blankLine);
myMovies.push_back(aMovie);
} while ( !fin.eof() );
fin.close();
}
This just leaves the searchByDirector() method for us to define.
To build our searchByDirector() method, we might:
vector<Movie> MovieCollection::searchByDirector(const string& directorName) const {
vector<Movie> v;
for (unsigned i = 0; i < myMovies.size(); i++) {
if ( myMovies[i].getDirector().find(directorName) != string::npos ) {
v.push_back( myMovies[i] );
}
}
return v;
}
This should remove the remaining errors, so that when you compile and run the test-application,
all tests should be passed:
... Testing Movie class... - Default Constructor ... Passed! - Explicit Constructor... Passed! - readFrom()... Passed! - writeTo()... 1 2 Passed! All tests passed! Testing class MovieCollection... - constructor... 1 2 3 Passed! All tests passed!
Congratulations! You now have a working data structure that we can use to solve (a part of) Phil's problem!
This week's project is to finish the data structure and solve the rest of Phil's problem.
The folder /home/cs/112/current should contain a folder with your user-name. You can check by typing this command into a terminal window:
$ ls /home/cs/112/currentEach week's lab and project must be copied to your folder, so that the grader can grade it. If your project is not in your folder within /home/cs/112/current, the grader will not be able to find it, and you will receive a zero!
To illustrate, suppose that your user-name is abc1, that you are currently in your home folder, that your Eclipse workspace is named cs112, that your cs112 folder is within your home folder, and that your lab1 folder is inside your cs112 folder (i.e., /home/abc1/cs112/lab1). Then you should enter:
$ cd cs112
to change directory to the folder "above" your lab1 folder, and then enter a command like this:
$ cp -r lab1 /home/cs/112/current/abc1
where you replace abc1 with your user-name. The cp -r command will recursively copy lab1 and all of its subfolders into your folder. You can check that this worked by listing the contents of your folder:
$ ls /home/cs/112/current/abc1
lab1
and then verifying that your folder contains a copy of the today's files:
$ ls /home/cs/112/current/abc1/lab1 ... contents of lab1 should appear ...
When your folder within current contains a copy of your lab work, you are all done. Congratulations!
If time permits, you may begin working on this week's project.