... but test everything; hold fast to what is good...
-- 1 Thessalonians 5:21
You've been hired by a new startup company that hopes to revolutionize the music streaming business. You have been assigned the task of writing the software the company will use for its song playlists.
The songs in a playlist will be stored in a text file whose name is the name of the playlist (e.g., MyFavorites.txt). When a given user chooses a playlist, your software needs to open the file and read the song information from the file into memory. Once the song information is in memory, your software needs to let the user:
To begin solving this problem, we will
We will also write test classes and a testing application to try to ensure that each of these classes works as expected.
Create a new project named lab01 in which to store the files related to this project. With that project selected, use Project > Properties > C/C++ Build > Behavior > Build on resource save to automatically build your project each time you save it. You will need to do this for every Eclipse project, so make a habit of doing it each time you create a new project.
As described previously, we want to test our software to ensure that it works as expected. Your boss has supplied you with the following testSongs playlist, so inside this project, create a new text file named testSongs.txt containing the following:
Call Me Maybe Carly Rae Jepsen 2012 --- Let It Be The Beatles 1967 --- Let It Be Joan Baez 1971 --- Penny Lane The Beatles 1967 ---
Note the structure of this song and playlist information. A song has its title, artist/group, and year, each on separate lines. Within the playlist, each song is followed by a --- line to separate it from the next one. This playlist file structure will determine how we write some of our methods.
(In this diagram, the diamond-shaped symbols denote the "has a" relationship.)
We will thus be creating nine files during today's exercise. Let's get started!
Since a playlist is made up of songs, we will begin with the Song class (sort of). First, we will create our testing application, then a SongTester class, and finally the Song class itself.
To run our tests, we can create a source file named main.cpp containing a simple main() function:
/* main.cpp tests the classes in our project.
* Student 1 Name and UserId:
* Student 2 Name and UserId:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#include "SongTester.h"
int main() {
SongTester sTester;
sTester.runTests();
}
Be sure you customize the opening documentation by adding your names, user ids, and the date.
Every C++ source file you create for this course
should contain such opening documentation that indicates:
However, when we try to build this program, we get compilation errors. The problem is that there is no class SongTester nor header file SongTester.h.
To fix the compilation errors, we must create a SongTester class to store the tests we will run on the methods of our Song class. Create a new class named SongTester that looks like this at the outset (again, customizing its opening documentation):
/* SongTester.h declares a test-class for class Song.
* Student 1 Name and Username:
* Student 2 Name and Username:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#ifndef SONGTESTER_H_
#define SONGTESTER_H_
class SongTester {
public:
void runTests();
};
#endif /*SONGTESTER_H_*/
Generally speaking, C++ header (.h) files contain class and method/function declarations; function definitions are placed in the implementation (.cpp) files. At present, our SongTester class contains a prototype or declaration of a single method named runTests(). (We'll be adding other method prototypes shortly.)
A C++ class usually consists of two files:
/* SongTester.cpp defines the test-methods for class SongTester.
* Student 1 Name and Username:
* Student 2 Name and Username:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#include "SongTester.h"
#include <iostream>
using namespace std;
void SongTester::runTests() {
cout << "Testing class Song..." << endl;
cout << "All tests passed!" << endl;
}
Note that since we have no actual tests to run (yet), we just provide a minimal definition for our runTests() method for now.
At this point, you should able to build your project without errors, and run it to produce the messages:
Testing class Song... All tests passed!
If you have errors, find and fix them before continuing.
We should be able to declare Song objects with the expectation that they will be initialized to a sane default value. Such initialization is the job of a class's default constructor.
If we choose "empty" strings and zero values as our default values, we might build a test method in SongTester.cpp to test our constructors as follows:
void SongTester::testConstructors() {
cout << "- constructors ... " << flush;
// default constructor
Song s;
assert( s.getTitle() == "" );
assert( s.getArtist() == "" );
assert( s.getYear() == 0 );
cout << " 0 " << flush;
cout << " Passed!" << endl;
}
That is, we expect to be able to send a Song object messages like
getTitle(), getArtist(), and so on, to access the values of its
instance variables, so we use those to test its values.
In order for this test-method to be a method of class SongTester, we must add a prototype for the method to our SongTester class:
class SongTester {
public:
void runTests();
void testConstructors();
};
You will also need to invoke this method in our SongTester::runTests() method (in SongTester.cpp):
void SongTester::runTests() {
cout << "Testing class Song..." << endl;
testConstructors();
cout << "All tests passed!" << endl;
}
and finally #include any header files
this method requires (e.g., <cassert>).
Next, try to build your project. The compiler should generate an error message something like the following:
../SongTester.cpp: In member function 'void SongTester::testConstructors()':
../SongTester.cpp:21:5: error: 'Song' was not declared in this scope
Song s;
...
Note that this is expected, as we have not yet created our Song class.
Let's do so!
From the structure of our boss's test file, our Song class needs to store these attributes about a song:
Instance variables should generally be declared as private:, to keep developers from writing software that depend upon them, so we can start by creating the following class declaration in Song.h:
/* Song.h declares a class for storing song information.
* Student 1 Name and Username:
* Student 2 Name and Username:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#ifndef SONG_H
#define SONG_H
#include <string>
using namespace std;
class Song {
public:
private:
string myTitle;
string myArtist;
unsigned myYear;
};
#endif /*SONG_H_*/
Since our SongTester class is trying to create a Song object, it must see the declaration of class Song. To do so, it must #include "Song.h" so take a moment to add that directive to SongTester.cpp.
Since a C++ class has both a header file and an implementation file, so we also need to create the file Song.cpp:
/* Song.cpp defines the methods for class Song (see Song.h).
* Student 1 Name and Username:
* Student 2 Name and Username:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#include "Song.h"
As the file's header comment indicates, Song.cpp is where we will define the methods of our Song class. The SongTester::testConstructors() method invokes our Song default constructor, plus three Song "getter" methods. The test-method tells us the default values the Song class should provide, and since it is the role of the default constructor to initialize the members to these values, we might define these four operations as follows:
/* Song default constructor
* Postcondition: myTitle and myArtist are empty strings
* && myYear == 0.
*/
Song::Song() {
myTitle = myArtist = "";
myYear = 0;
}
/* getter method for myTitle
* Return: myTitle
*/
string Song::getTitle() const {
return myTitle;
}
/* getter method for myArtist
* Return: myArtist
*/
string Song::getArtist() const {
return myArtist;
}
/* getter method for myYear
* Return: myYear
*/
unsigned Song::getYear() const {
return myYear;
}
Note that a constructor has no return-type (not even void).
Each of the other operations requires a return-type
appropriate to the value the test indicates the method is returning.
We must then add prototypes for these methods to Song.h:
class Song {
public:
Song();
string getTitle() const;
string getArtist() const;
unsigned getYear() const;
private:
string myTitle;
string myArtist;
unsigned myYear;
};
Now that we have a Song class created, we must #include "Song.h" in our SongTester.cpp file, so that our testConstructors() method can declare its Song..
With that change, our project should compile without any errors.
Testing class Song... - constructors ... 0 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 define the methods needed to pass 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 method or class do not "break" code that previously worked correctly.
In addition to a default constructor, most classes should provide an explicit-value constructor that lets the programmer initialize an object's members to specific values. For example, we might invoke such a constructor for our Song class by adding the following code to our testConstructors() method:
void SongTester::testConstructors() {
cout << "- constructors... " << flush;
// default constructor
Song s;
assert( s.getTitle() == "" );
assert( s.getArtist() == "" );
assert( s.getYear() == 0 );
cout << " 0 " << flush;
// explicit constructor
Song s1("Badge", "Cream", 1969);
assert( s1.getTitle() == "Badge" );
assert( s1.getArtist() == "Cream" );
assert( s1.getYear() == 1969 );
cout << " 1 " << flush;
cout << "Passed!" << endl;
}
Next, try to build your project. As expected, we get a compilation error because the compiler cannot (yet) find a prototype or definition for our explicit-value constructor:
../SongTester.cpp: In member function 'void SongTester::testConstructors()':
../SongTester.cpp:28:35: error: no matching function for call to 'Song::Song(const char [6], const char [6], int)'
Song s1("Badge", "Cream", 1969);
...
To eliminate this error, we must define the explicit-value constructor!
Once again, we write a method to satisfy our test. The test indicates the order in which the arguments are passed to the constructor, so add this definition to the definitions in Song.cpp:
/* Explicit constructor
* @param: title, a string
* @param: artist, a string
* @year: an unsigned int.
* Postcondition: myTitle == title &&
* myArtist == artist &&
* myYear == year.
*/
Song::Song(const string& title, const string& artist, unsigned year) {
myTitle = title;
myArtist = artist;
myYear = year;
}
Note that since title and artist are string objects whose values flow into but not out of the constructor, we pass them using pass-by-const-reference. By contrast, year is an unsigned int, which is a primitive type, so we pass it using pass-by-value.
Next, add a prototype for this constructor to our Song class. You should now have two constructors declared there: a default-value constructor and an explicit-value constructor. When you have done so, our test-program should compile and run correctly:
Testing class Song... - constructors ... 0 1 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:
Song::Song(const string& title, const string& artist, unsigned year) {
myTitle = title;
myArtist = artist;
//myYear = year;
}
Now when our test-method runs, the test for the explicit-value constructor will fail:
Testing class Song... - constructors ... 0 Assertion failed: (s1.getYear() == 1969), function testConstructors, file ../SongTester.cpp, line 31.By checking whether or not your methods pass the tests, test-driven development provides a methodology to help you keep from making mistakes.
"Uncomment" the myYear = year; line in your constructor, rebuild your project, and double-check that your code passes the tests before continuing.
We can then define a new test-method in SongTester.cpp to test our input method. We will call our input method readFrom(), so we name our test-method testReadFrom():
void SongTester::testReadFrom() {
cout << "- readFrom()... " << flush;
ifstream fin("testSongs.txt");
assert( fin.is_open() );
Song s;
string separator;
// read first song and separator in test playlist
s.readFrom(fin);
getline(fin, separator);
assert( s.getTitle() == "Call Me Maybe" );
assert( s.getArtist() == "Carly Rae Jepsen" );
assert( s.getYear() == 2012 );
cout << " 0 " << flush;
// read second song and separator in test playlist
s.readFrom(fin);
getline(fin, separator);
assert( s.getTitle() == "Let It Be" );
assert( s.getArtist() == "The Beatles" );
assert( s.getYear() == 1967 );
cout << " 1 " << flush;
// read third song and separator in test playlist
s.readFrom(fin);
getline(fin, separator);
assert( s.getTitle() == "Let It Be" );
assert( s.getArtist() == "Joan Baez" );
assert( s.getYear() == 1971 );
cout << " 2 " << flush;
fin.close();
cout << "Passed!" << endl;
}
Using this as a model, add a final test to this method
that reads the fourth song and separator from our test playlist,
checks that the song has the correct values,
and if so, displays the value 3.
Note how the testReadFrom() method deals with the separator lines that follow each song. Make certain your test-code handles these the same way.
Then try to build your project. You should see several error messages. As before, we must:
Note that our test method invokes readFrom() multiple times. It is generally a good idea to test a method multiple times, as invoking a method multiple times will sometimes reveal errors that do not show up when the method is only invoked once.
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:
/* Song input method...
* @param: in, an istream
* Precondition: in contains the title, artist, and year data for a Song.
* Postcondition: the title, artist, and year data have been read from in &&
* myTitle == title &&
* myArtist == artist &&
* myYear == year.
*/
void Song::readFrom(istream& in) {
getline(in, myTitle);
getline(in, myArtist);
string yearString;
getline(in, yearString);
myYear = atoi( yearString.c_str() );
}
Note that because our method modifies the stream it receives throught its parameter in, we declare that parameter using pass-by-reference.
Note also that we use the getline() method to read in the various string values. We do this because getline() will read an entire line of text, whereas the string version of operator>> will only read a word of text.Note finally that for simplicity's sake, we also use getline() to read the year value; however getline() only reads strings, so we read the year as a string into a string variable named yearString. To store this in our year instance variable, we must convert this string to an integer. One way to do this is to use the C function atoi(), which stands for ascii-to-integer. However, being a C function, atoi() requires a C-style string, not a C++-style string. To convert yearString to a C-style string, we can send it the c_str() message, which retrieves the C-style string from within yearString. We then pass that C-style string to the atoi() function, which converts that C-style string to the corresponding integer value. Whew!
In order for this method to compile correctly, add a prototype for this method to the Song class, as well as any #include directives you need in order for this method to compile correctly (e.g., atoi() is declared in <cstdlib>, istream is declared in <iostream>).
Once our program compiles correctly, run it and verify that it passes all our tests:
Testing class Song... - constructors ... 0 1 Passed! - readFrom()... 0 1 2 3 Passed! All tests passed!
Once we have passed all our tests, we can continue to the next operation.
Testing our writeTo() method using assert() takes some effort:
void SongTester::testWriteTo() {
cout << "- writeTo()... " << flush;
// declare three songs
Song s1("Badge", "Cream", 1969);
Song s2("Godzilla", "Blue Oyster Cult", 1977);
Song s3("Behind Blue Eyes", "The Who", 1971);
// write the three songs to an output file
ofstream fout("testSongOutput.txt");
assert( fout.is_open() );
s1.writeTo(fout);
fout << "---\n";
s2.writeTo(fout);
fout << "---\n";
s3.writeTo(fout);
fout << "---\n";
fout.close();
// use readFrom() to see if writeTo() worked
ifstream fin("testSongOutput.txt");
assert( fin.is_open() );
Song s4, s5, s6;
string separator;
// read and check the first song
s4.readFrom(fin);
getline(fin, separator);
assert( s4.getTitle() == "Badge" );
assert( s4.getArtist() == "Cream" );
assert( s4.getYear() == 1969 );
cout << " 0 " << flush;
// read and check the second song
s5.readFrom(fin);
getline(fin, separator);
assert( s5.getTitle() == "Godzilla" );
assert( s5.getArtist() == "Blue Oyster Cult" );
assert( s5.getYear() == 1977 );
cout << " 1 " << flush;
// read and check the third song
s6.readFrom(fin);
getline(fin, separator);
assert( s6.getTitle() == "Behind Blue Eyes" );
assert( s6.getArtist() == "The Who" );
assert( s6.getYear() == 1971 );
cout << " 2 " << flush;
fin.close();
cout << " Passed!" << endl;
}
There are a few things worth noting about this test:
After placing a prototype of testWriteTo() in our SongTester class and invoking this method in runTests(), the only error we should generate is due to writeTo() not being defined:
../SongTester.cpp: In member function 'void SongTester::testWriteTo()':
../SongTester.cpp:72:8: error: 'class Song' has no member named 'writeTo'
s1.writeTo(fout);
^
...
This means it is time for us to define that method!
/* Song output...
* @param: out, an ostream
* Postcondition: out contains myTitle, a newline,
* myArtist, a newline,
* myYear, and a newline.
*/
void Song::writeTo(ostream& out) const {
out << myTitle << '\n'
<< myArtist << '\n'
<< myYear << '\n';
}
Note that our method changes its parameter out
so we define that parameter using call-by-reference.
Note also that an output method should not change any of its class's instance variables, so we define writeTo() as a const method. This tells the compiler that if we should inadvertantly change an instance variable in this method, the compiler should generate an error to alert us to our mistake.
Define this method and place a prototype for it in your Song class. Then compile and run your test-application. If all is well, pass all the tests should pass.
Congratulations! You have just built a reasonably complex Song class!
Our next class is the PlayList class, in which we will store the songs of a given playlist. Since it must store multiple songs, we might envision a PlayList object named aPlayList containing the songs from testSongs.txt as follows:
To test this new class, we will create a PlayListTester class, similar to the SongTester class we built previously.
We start by returning to main.cpp and adding code that (a) #includes the header file PlayListTester.h, (b) creates a PlayListTester object, and (c) invokes its runTests() method:
#include "SongTester.h"
#include "PlayListTester.h"
int main() {
SongTester sTester;
sTester.runTests();
PlayListTester plTester;
plTester.runTests();
}
If we try to build our project, we get errors because neither class PlayListTester nor the file PlayListTester.h exist. To proceed, we must create them, which we might do as follows:
/* PlayListTester.h tests the PlayList class.
* Student 1 Name and Username:
* Student 2 Name and Username:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#ifndef PLAYLISTTESTER_
#define PLAYLISTTESTER_
class PlayListTester {
public:
void runTests();
};
#endif /*PLAYLISTTESTER_*/
and
/* PlayListTester.cpp defines the PlayList test-methods.
* Student 1 Name and Username:
* Student 2 Name and Username:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#include "PlayListTester.h"
#include <iostream>
using namespace std;
void PlayListTester::runTests() {
cout << "\nTesting class PlayList..." << endl;
cout << "All tests passed!" << endl;
}
This should be sufficient to allow our project to build and run successfully:
Testing class Song... - constructors ... 0 1 Passed! - readFrom()... 0 1 2 3 Passed! - writeTo()... 0 1 2 Passed! All tests passed! Testing class PlayList... All tests passed!
Note that we do not replace our SongTester tests. We want to continue to continue to re-run the Song tests each time we test our PlayList methods, to make sure that nothing we write breaks anything 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.
Recall that a playlist's song-data is stored in a file. Because of this, it really doesn't make much sense to build a default-constructor for our PlayList class, at least not yet. Instead, we will build an explicit constructor to which we pass the name of a playlist file as an argument. Our constructor can get the name of the file via its parameter, and then read the playlist's data from that file.
To test our constructor, we can once again use the testSongs.txt file that our boss provided. We can then write a method to test our PlayList constructor:
void PlayListTester::testConstructors() {
cout << "- constructors..." << flush;
PlayList pList("testSongs.txt");
assert( pList.getNumSongs() == 4 );
cout << " 0 " << flush;
cout << " Passed!" << endl;
}
Note that our test method (a) invokes an explicit-value PlayList constructor, passing it the name of our playlist file; and (b) checks that this worked by checking that the PlayList contains four songs, the number in the playlist file.
We must of course invoke this method in runTests():
void PlayListTester::runTests() {
cout << "Testing class PlayList..." << endl;
testConstructors();
cout << "All tests passed!" << endl;
}
and place its prototype in our PlayListTester class:
class PlayListTester {
public:
void runTests();
void testConstructors();
};
When we try to build, the compiler will generate errors because we have not yet defined a PlayList class. That means it's time to do so!
Given the functionality required by our testConstructors() method, we might declare class PlayList as follows:
/* PlayList.h declares class PlayList.
* Student 1 Name and Username:
* Student 2 Name and Username:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#ifndef PLAYLIST_H_
#define PLAYLIST_H_
#include <string>
using namespace std;
class PlayList {
public:
PlayList(const string& fileName);
unsigned getNumSongs() const;
};
#endif /*PLAYLIST_H_*/
Note that we declare an explicit-value constructor, which has a parameter
to store the name of the playlist file; and
a getNumSongs() method, since the test requires that.
Note also that since the parameter for our PlayList constructor is a string (which is a class type), we declare it using pass-by-const-reference.
Note finally that since the getNumSongs() method should not change any of our instance variables, we declare it as a const method.
Take a moment to #include "PlayList.h" in PlayListTester.cpp, so that our test-methods can see class PlayList; and <cassert> so that they can use assert().
If you try to build the project at this point, there will still be errors because we have declared the PlayList() explicit-value constructor and the getNumSongs() method, but we have not defined them. We can fix that by adding the following stub definitions to PlayList.cpp:
/* PlayList.cpp defines the PlayList methods.
*
* Student 1 Name and Username:
* Student 2 Name and Username:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#include "PlayList.h"
/* PlayList constructor
* @param: fileName, a string
* Precondition: fileName contains the name of a playlist file.
*/
PlayList::PlayList(const string& fileName) {
}
/* Retrieve length of the playlist
* Return: the number of songs in the playlist.
*/
unsigned PlayList::getNumSongs() const {
}
With that addition, our program should build without errors.
Make sure that this is the case before proceeding.
While our program builds, when we try to run it, it fails our test:
Testing class Song... - constructors ... 0 1 Passed! - readFrom()... 0 1 2 3 Passed! - writeTo()... 0 1 2 Passed! All tests passed! Testing class PlayList... - constructors...Assertion failed: (pList.getNumSongs() == 4), function testConstructors, file ../PlayListTester.cpp, line 22.
To pass the test, we must fill in the stubs of our PlayList constructor and getNumSongs() methods.
Before we can complete the constructor definition, we have to decide how we are going to store the playlist's Song objects. One way we might do so is using the vector class-template from the C++ standard template library (STL). To do this, we can add an instance variable to our PlayList class whose type is vector<Song> and whose name is mySongs, as shown below:
/* PlayList.h declares class PlayList.
* Student 1 Name and Username:
* Student 2 Name and Username:
* Date:
* Begun by: Joel Adams, for CS 112 at Calvin University.
*/
#ifndef PLAYLIST_H_
#define PLAYLIST_H_
#include "Song.h"
#include <vector> // STL vector
#include <string>
using namespace std;
class PlayList {
public:
PlayList(const string& fileName);
unsigned getNumSongs() const;
private:
vector<Song> mySongs;
};
#endif /*PLAYLIST_H_*/
A vector is an array that can grow or shrink as the program runs. For this reason, a vector is sometimes called a dynamic array.
With a vector data structure in which to store our songs, we can define our PlayList() constructor as follows:
/* PlayList.cpp ...
* ...
*/
#include "PlayList.h"
#include <fstream> // ifstream
#include <cassert> // assert()
using namespace std;
/* PlayList constructor
* @param: fileName, a string
* Precondition: fileName contains the name of a playlist file.
*/
PlayList::PlayList(const string& fileName) {
// open a stream to the playlist file
ifstream fin( fileName.c_str() );
assert( fin.is_open() );
// read each song and append it to mySongs
Song s;
string separator;
while (true) {
s.readFrom(fin);
if ( !fin ) { break; }
getline(fin, separator);
mySongs.push_back(s);
}
// close the stream
fin.close();
}
Note that this method uses a vector method named push_back() to append a Song object to our mySongs instance variable. If the vector is full, the push_back() method makes it bigger to accomodate the value being appended.
Make certain that this much builds correctly before proceeding.
To pass our test, our getNumSongs() method just needs to return the number of songs in mySongs. Since mySongs is a vector, we can use the vector method size(), which returns the number of items in a vector. We can thus define getNumSongs() as follows:
/* Retrieve length of the playlist
* Return: the number of songs in the playlist.
*/
unsigned PlayList::getNumSongs() const {
return mySongs.size();
}
With that change, your program should build correctly, and when you run it, it should pass all our tests. Make certain this is the case before proceeding.
Last but not least, let's see how to implement one of the operations our boss told us our software must support. The boss said we must be able to search a playlist for all the songs by a given artist, so let's build a test for this. Here is one way we might do so:
void PlayListTester::testSearchByArtist() {
cout << "- searchByArtist()... " << flush;
// load a playlist with test songs
PlayList pList("testSongs.txt");
// empty case (0 artists)
vector<Song> searchResult = pList.searchByArtist("Cream");
assert( searchResult.size() == 0 );
cout << " 0 " << flush;
// case of 1 artist
searchResult = pList.searchByArtist("Baez");
assert( searchResult.size() == 1 );
assert( searchResult[0].getTitle() == "Let It Be" );
cout << " 1 " << flush;
// case of 2 artists
searchResult = pList.searchByArtist("Beatles");
assert( searchResult.size() == 2 );
assert( searchResult[0].getTitle() == "Let It Be" );
assert( searchResult[1].getTitle() == "Penny Lane" );
cout << " 2 " << flush;
cout << " Passed!" << endl;
}
Since searchByArtist() needs to return all songs in our playlist by that artist, we need a data structure to store those songs. We thus use a vector<Song> to store this info. This implies that searchByArtist() must return a vector<Song>.
Note also that to exercise the various possibilities, our method tests:
Add a call to testSearchByArtist() to the PlayListTester::runTests() method. Then save and build your project.
When you try to build your project, the compiler will not find a declaration of this test-method in our PlayListTester class, so you will see an error message something like this:
../PlayListTester.cpp:28:41: error: no 'void PlayListTester::testSearchByArtist()' member function declared in class 'PlayListTester'
void PlayListTester::testSearchByArtist() {
To fix this error message,
add a prototype for this method to PlayListTester.h.
When you then try to rebuild your project, the compiler will be unable to find searchByArtist() method in class PlayList:
../PlayListTester.cpp: In member function 'void PlayListTester::testSearchByArtist()':
../PlayListTester.cpp:34:41: error: 'class PlayList' has no member named 'searchByArtist'
vector searchResult = pList.searchByArtist("Cream");
That means it's time for us to define the searchByArtist() method!
To build our searchByArtist() method, we might:
/* Search by artist
* @param: artist, a string.
* Return: a vector containing all the Songs in mySongs by artist.
*/
vector<Song> PlayList::searchByArtist(const string& artist) const {
vector<Song> v;
for (unsigned i = 0; i < mySongs.size(); ++i) {
if ( mySongs[i].getArtist().find(artist) != string::npos ) {
v.push_back( mySongs[i] );
}
}
return v;
}
Note that:
Note also that given the way find() works, a call like:
searchByArtist("Beatles")
will still match songs for which getArtist() returns "The Beatles".
If we had written:
...
for (unsigned i = 0; i < mySongs.size(); ++i) {
if ( mySongs[i].getArtist() == artist ) {
v.push_back( mySongs[i] );
}
}
...
then the argument to searchByArtist() would have to
exactly match
the string returned by mySongs[i].getArtist().
For example,
searchByArtist("The Beatles")
would find the Beatles songs in our playlist, but
searchByArtist("Beatles")
would not.
That is the reason we are using the find() method
instead of the equality operator.
As always, you will need to add a prototype for this method to class PlayList, so take a moment to do so.
This should remove the remaining errors, so that when you compile and run the test-application, all tests should be passed:
Testing class Song... - constructors ... 0 1 Passed! - readFrom()... 0 1 2 3 Passed! - writeTo()... 0 1 2 Passed! All tests passed! Testing class PlayList... - constructors... 0 Passed! - searchByArtist()... 0 1 2 Passed! All tests passed!
Congratulations! You now have a working data structure that we can use to store playlists for our company!
This week's project is to finish the data structure and build the other playlist operations.
If you used pair programming, only one of you should turn in your code. It does not matter which
person turns it in, but
--> make sure you have both students' names and user-names in the header
documentation in the files. <--
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 (separately) copied to your folder, so that the grader can grade it. If your lab is not copied to 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 lab01 folder is inside your cs112 folder (i.e., /home/abc1/cs112/lab01). Then you should enter:
$ cd cs112
to change directory to your workspace -- the folder "above" your lab01 folder. Then enter:
$ lsto view the contents of your workspace. At this point, you should see folders for last week's lab (lab0) and this week's lab (lab01). Then enter:
$ cp -r lab01 /home/cs/112/current/abc1
where you replace abc1 with your user-name. The cp -r command will recursively copy lab01 and all of its subfolders into your folder inside /home/cs/112/current/. You can check that this worked by listing the contents of your folder:
$ ls /home/cs/112/current/abc1
lab01
and then verifying that your folder contains a copy of the today's files:
$ ls /home/cs/112/current/abc1/lab01 ... files for lab01 should appear ...
We will be using these same commands to submit our labs and projects each week for the rest of the semester, so you should record them somewhere that you can easily find them (or maybe bookmark this page).
When your folder within current contains a copy of your lab work, you are all done. Congratulations!
Next, you should email all your files to your partner, so that she/he can work independently on the project.
If time permits, you may each begin working on this week's project.