In a data-oriented system, our code deals mainly with data manipulation: most of our functions receive data and return data. As a consequence, it’s quite easy to write unit tests to check whether our code behaves as expected. A unit test is made of test cases that generate data input and compare the data output of the function with the expected data output. In this chapter, we write unit tests for the queries and mutations that we wrote in the previous chapters.
Theo and Joe are seated around a large wooden table in a corner of “La vie est belle,” a nice little French coffee shop, located near the Golden Gate Bridge. Theo orders a café au lait with a croissant, and Joe orders a tight espresso with a pain au chocolat. Instead of the usual general discussions about programming and life when they’re out of the office, Joe leads the discussion towards a very concrete topic—unit tests. Theo asks Joe for an explanation.
THEO Are unit tests such a simple topic that we can tackle it here in a coffee shop?
JOE Unit tests in general, no. But unit tests for data-oriented code, yes!
THEO Why does that make a difference?
JOE The vast majority of the code base of a data-oriented system deals with data manipulation.
THEO Yeah. I noticed that almost all the functions we wrote so far receive data and return data.
?Tip Most of the code in a data-oriented system deals with data manipulation.
JOE Writing a test case for functions that deal with data is only about generating data input and expected output, and comparing the output of the function with the expected output.
JOE Yes. As you’ll see in a moment, in DOP, there’s usually no need for mock functions.
THEO I understand how to compare primitive values like strings or numbers, but I’m not sure how I would compare data collections like maps.
JOE You compare field by field.
THEO Oh no! I’m not able to write any recursive code in a coffee shop. I need the calm of my office for that kind of stuff.
JOE Don’t worry. In DOP, data is represented in a generic way. There is a generic function in Lodash called _.isEqual
for recursive comparison of data collections. It works with both maps and arrays.
Joe opens his laptop. He is able to convince Theo by executing a few code snippets with _.isEqual
to compare an equal data collection with a non-equal one.
_.isEqual({ "name": "Alan Moore", "bookIsbns": ["978-1779501127"] }, { "name": "Alan Moore", "bookIsbns": ["978-1779501127"] }); // → true
_.isEqual({ "name": "Alan Moore", "bookIsbns": ["978-1779501127"] }, { "name": "Alan Moore", "bookIsbns": ["bad-isbn"] }); // → false
JOE Most of the test cases in DOP follow this pattern.
Theo decides he wants to try this out. He fires up his laptop and types a few lines of pseudocode.
?Tip It’s straightforward to write unit tests for code that deals with data manipulation.
THEO Indeed, this looks like something we can tackle in a coffee shop!
A waiter in an elegant bow tie brings Theo his croissant and Joe his pain au chocolat. The two friends momentarily interrupt their discussion to savor their French pastries. When they’re done, they ask the waiter to bring them their drinks. Meanwhile, they resume the discussion.
JOE Do you remember the code flow of the implementation of the search query?
THEO Let me look again at the code that implements the search query.
Theo brings up the implementation of the search query on his laptop. Noticing that Joe is chewing on his nails again, he quickly checks out the code.
class Catalog { static authorNames(catalogData, authorIds) { return _.map(authorIds, function(authorId) { return _.get(catalogData, ["authorsById", authorId, "name"]); }); } static bookInfo(catalogData, book) { var bookInfo = { "title": _.get(book, "title"), "isbn": _.get(book, "isbn"), "authorNames": Catalog.authorNames(catalogData, _.get(book, "authorIds")) }; return bookInfo; } static searchBooksByTitle(catalogData, query) { var allBooks = _.get(catalogData, "booksByIsbn"); var matchingBooks = _.filter(allBooks, function(book) { return _.get(book, "title").includes(query); }); var bookInfos = _.map(matchingBooks, function(book) { return Catalog.bookInfo(catalogData, book); }); return bookInfos; } } class Library { static searchBooksByTitleJSON(libraryData, query) { var catalogData = _.get(libraryData, "catalog"); var results = Catalog.searchBooksByTitle(catalogData, query); var resultsJSON = JSON.stringify(results); return resultsJSON; } }
The waiter brings Theo his café au lait and Joe his tight espresso. They continue their discussion while enjoying their coffees.
JOE Before writing a unit test for a code flow, I find it useful to visualize the tree of function calls of the code flow.
THEO What do you mean by a tree of function calls?
JOE Here, I’ll draw the tree of function calls for the Library.searchBooksByTitleJSON
code flow.
Joe puts down his espresso and takes a napkin from the dispenser. He carefully places it flat on the table and starts to draw. When he is done, he shows the illustration to Theo (see figure 6.1).
THEO Nice! Can you teach me how to draw a tree of function calls like that?
JOE Sure. The root of the tree is the name of the function for which you draw the tree, in our case, Library.searchBooksByTitleJSON
. The children of a node in the tree are the names of the functions called by the function. For example, if you look again at the code for Library.searchBooksByTitleJSON
(listing 6.4), you’ll see that it calls Catalog.searchBooksByTitle
, _.get
, and JSON.stringify
.
THEO How long would I continue to recursively expand the tree?
JOE You continue until you reach a function that doesn’t belong to the code base of your application. Those nodes are the leaves of our tree; for example, the functions from Lodash
: _.get
, _.map
, and so forth.
THEO What if the code of a function doesn’t call any other functions?
JOE A function that doesn’t call any other function would be a leaf in the tree.
THEO What about functions that are called inside anonymous functions like Catalog .bookInfo
?
JOE Catalog.bookInfo
appears in the code of Catalog.searchBooksByTitle
. Therefore, it is considered to be a child node of Catalog.searchBooksByTitle
. The fact that it is nested inside an anonymous function is not relevant in the context of the tree of function calls.
►Note A tree of function calls for a function f
is a tree where the root is f
, and the children of a node g
in the tree are the functions called by g
. The leaves of the tree are functions that are not part of the code base of the application. These are functions that don’t call any other functions.
THEO It’s very cool to visualize my code as a tree, but I don’t see how it relates to unit tests.
JOE The tree of function calls guides us about the quality and the quantity of test cases we should write.
JOE Let’s start from the function that appears in the deepest node in our tree: Catalog.authorNames
. Take a look at the code for Catalog.authorNames
and tell me what are the input and the output of Catalog.authorNames
.
Joe turns his laptop so Theo can a closer look at the code. Theo takes a sip of his café au lait as he looks over what’s on Joe’s laptop.
Catalog.authorNames = function (catalogData, authorIds) { return _.map(authorIds, function(authorId) { return _.get(catalogData, ["authorsById", authorId, "name"]); }); };
THEO The input of Catalog.authorNames
is catalogData
and authorIds
. The output is authorNames
.
JOE Would you do me a favor and express it visually?
It’s Theo’s turn to grab a napkin. He draws a small rectangle with two inward arrows and one outward arrow as in figure 6.2.
JOE Excellent! Now, how many combinations of input would you include in the unit test for Catalog.authorNames
?
Theo reaches for another napkin. This time he creates a table to gather his thoughts (table 6.1).
THEO To begin with, I would have a catalogData
with two author IDs and call Catalog.authorNames
with three arguments: an empty array, an array with a single author ID, and an array with two author IDs.
JOE How would you generate the catalogData
?
THEO Exactly as we generated it before.
Turning to his laptop, Theo writes the code for catalogData
. He shows it to Joe.
var catalogData = { "booksByIsbn": { "978-1779501127": { "isbn": "978-1779501127", "title": "Watchmen", "publicationYear": 1987, "authorIds": ["alan-moore", "dave-gibbons"], "bookItems": [ { "id": "book-item-1", "libId": "nyc-central-lib", "isLent": true }, { "id": "book-item-2", "libId": "nyc-central-lib", "isLent": false } ] } }, "authorsById": { "alan-moore": { "name": "Alan Moore", "bookIsbns": ["978-1779501127"] }, "dave-gibbons": { "name": "Dave Gibbons", "bookIsbns": ["978-1779501127"] } } };
JOE You could use your big catalogData
map for the unit test, but you could also use a smaller map in the context of Catalog.authorNames
. You can get rid of the booksByIsbn
field of the catalogData
and the bookIsbns
fields of the authors.
Joe deletes a few lines from catalogData
and gets a much smaller map. He shows the revision to Theo.
var catalogData = { "authorsById": { "alan-moore": { "name": "Alan Moore" }, "dave-gibbons": { "name": "Dave Gibbons" } } };
THEO Wait a minute! This catalogData
is not valid.
JOE In DOP, data validity depends on the context. In the context of Library .searchBooksByTitleJSON
and Catalog.searchBooksByTitle
, the minimal version of catalogData
is indeed not valid. However, in the context of Catalog.bookInfo
and Catalog.authorNames
, it is perfectly valid. The reason is that those two functions access only the authorsById
field of catalogData
.
?Tip The validity of the data depends on the context.
THEO Why is it better to use a minimal version of the data in a test case?
JOE For a very simple reason—the smaller the data, the easier it is to manipulate.
?Tip The smaller the data, the easier it is to manipulate.
THEO I’ll appreciate that when I write the unit tests!
JOE Definitely! One last thing before we start coding: how would you check that the output of Catalog.authorNames
is as expected?
THEO I would check that the value returned by Catalog.authorNames
is an array with the expected author names.
JOE How would you handle the array comparison?
THEO Let me think. I want to compare by value, not by reference. I guess I’ll have to check that the array is of the expected size and then check member by member, recursively.
JOE That’s too much of a mental burden when you’re in a coffee shop. As I showed you earlier (see listing 6.1), we can recursively compare two data collections by value with _.isEqual
from Lodash.
?Tip We can compare the output and the expected output of our functions with _.isEqual
.
THEO Sounds good! Let me write the test cases.
Theo starts typing on his laptop. After a few minutes, he has some test cases for Catalog .authorNames
, each made from a function call to Catalog.authorNames
wrapped in _.isEqual
.
var catalogData = { "authorsById": { "alan-moore": { "name": "Alan Moore" }, "dave-gibbons": { "name": "Dave Gibbons" } } }; _.isEqual(Catalog.authorNames(catalogData, []), []); _.isEqual(Catalog.authorNames( catalogData, ["alan-moore"]), ["Alan Moore"]); _.isEqual(Catalog.authorNames(catalogData, ["alan-moore", "dave-gibbons"]), ["Alan Moore", "Dave Gibbons"]);
JOE Well done! Can you think of more test cases?
THEO Yes. There are test cases where the author ID doesn’t appear in the catalog data, and test cases with empty catalog data. With minimal catalog data and _.isEqual
, it’s really easy to write lots of test cases!
Theo really enjoys this challenge. He creates a few more test cases to present to Joe.
_.isEqual(Catalog.authorNames({}, []), []); _.isEqual(Catalog.authorNames({}, ["alan-moore"]), [undefined]); _.isEqual(Catalog.authorNames(catalogData, ["alan-moore", "albert-einstein"]), ["Alan Moore", undefined]); _.isEqual(Catalog.authorNames(catalogData, []), []); _.isEqual(Catalog.authorNames(catalogData, ["albert-einstein"]), [undefined]);
THEO How do I run these unit tests?
JOE You use your preferred test framework.
►Note We don’t deal here with test runners and test frameworks. We deal only with the logic of the test cases.
THEO I’m curious to see what unit tests for an upper node in the tree of function calls look like.
JOE Sure. Let’s write a unit test for Catalog.bookInfo
. How many test cases would you have for Catalog.bookInfo
?
Catalog.bookInfo = function (catalogData, book) { return { "title": _.get(book, "title"), "isbn": _.get(book, "isbn"), "authorNames": Catalog.authorNames(catalogData, _.get(book, "authorIds")) }; };
Theo takes another look at the code for Catalog.bookInfo
on his laptop. Then, reaching for another napkin, he draws a diagram of its input and output (see figure 6.3).
THEO I would have a similar number of test cases for Catalog.authorNames
: a book with a single author, with two authors, with existing authors, with non-existent authors, with ...
JOE Whoa! That’s not necessary. Given that we have already written unit tests for Catalog.authorNames
, we don’t need to check all the cases again. We simply need to write a minimal test case to confirm that the code works.
?Tip When we write a unit test for a function, we assume that the functions called by this function are covered by unit tests and work as expected. It significantly reduces the quantity of test cases in our unit tests.
JOE How would you write a minimal test case for Catalog.bookInfo
?
Theo once again takes a look at the code for Catalog.bookInfo
(see listing 6.10). Now he can answer Joe’s question.
THEO I would use the same catalog data as for Catalog.authorNames
and a book record. I’d test that the function behaves as expected by comparing its return value with a book info record using _.isEqual
. Here, let me show you.
It takes Theo a bit more time to write the unit test. The reason is that the input and the output of Catalog.authorNames
are both records. Dealing with a record is more complex than dealing with an array of strings (as it was the case for Catalog.authorNames
). Theo appreciates the fact that _.isEqual
saves him from writing code that compares the two maps property by property. When he’s through, he shows the result to Joe and takes a napkin to wipe his forehead.
var catalogData = { "authorsById": { "alan-moore": { "name": "Alan Moore" }, "dave-gibbons": { "name": "Dave Gibbons" } } }; var book = { "isbn": "978-1779501127", "title": "Watchmen", "publicationYear": 1987, "authorIds": ["alan-moore", "dave-gibbons"] }; var expectedResult = { "authorNames": ["Alan Moore", "Dave Gibbons"], "isbn": "978-1779501127", "title": "Watchmen", }; var result = Catalog.bookInfo(catalogData, book); _.isEqual(result, expectedResult);
JOE Perfect! Now, how would you compare the kind of unit tests for Catalog .bookInfo
with the unit tests for Catalog.authorNames
?
THEO On one hand, there is only a single test case in the unit test for Catalog.bookInfo
. On the other hand, the data involved in the test case is more complex than the data involved in the test cases for Catalog.authorNames
.
JOE Exactly! Functions that appear in a deep node in the tree of function calls tend to require more test cases, but the data involved in the test cases is less complex.
?Tip Functions that appear in a lower level in the tree of function calls tend to involve less complex data than functions that appear in a higher level in the tree (see table 6.2).
In the previous section, we saw how to write unit tests for utility functions like Catalog .bookInfo
and Catalog.authorNames
. Now, we are going to see how to write unit tests for the nodes of a query tree of function calls that are close to the root of the tree.
JOE Theo, how would you write a unit test for the code of the entry point of the search query?
To recall the particulars, Theo checks the code for Library.searchBooksByTitleJSON
. Although Joe was right about today’s topic being easy enough to enjoy the ambience of a coffee shop, he has been doing quite a lot of coding this morning.
Library.searchBooksByTitleJSON = function (libraryData, query) { var catalogData = _.get(libraryData, "catalog"); var results = Catalog.searchBooksByTitle(catalogData, query); var resultsJSON = JSON.stringify(results); return resultsJSON; };
He then takes a moment to think about how he’d write a unit test for that code. After another Aha! moment, now he’s got it.
THEO The inputs of Library.searchBooksByTitleJSON
are library data and a query string, and the output is a JSON string (see figure 6.4). So, I would create a library data record with a single book and write tests with query strings that match the name of the book and ones that don’t match.
JOE What about the expected results of the test cases?
THEO In cases where the query string matches, the expected result is a JSON string with the book info. In cases where the query string doesn’t match, the expected result is a JSON string with an empty array.
JOE Because your test case relies on a string comparison instead of a data comparison.
THEO What difference does it make? After all, the strings I’m comparing come from the serialization of data.
JOE It’s inherently much more complex to compare JSON strings than it is to compare data. For example, two different strings might be the serialization of the same piece of data.
JOE Take a look at these two strings. They are the serialization of the same data. They’re different strings because the fields appear in a different order, but in fact, they serialize the same data!
Joe turns his laptop to Theo. As Theo looks at the code, he realizes that, once again, Joe is correct.
var stringA = "{"title":"Watchmen","publicationYear":1987}"; var stringB = "{"publicationYear":1987,"title":"Watchmen"}";
?Tip Avoid using a string comparison in unit tests for functions that deal with data.
THEO I see... . Well, what can I do instead?
JOE Instead of comparing the output of Library.searchBooksByTitleJSON
with a string, you could deserialize the output and compare it to the expected data.
THEO What do you mean by deserialize a string?
JOE Deserializing a string s
, for example, means to generate a piece of data whose serialization is s
.
THEO Is there a Lodash function for string deserialization?
JOE Actually, there is a native JavaScript function for string deserialization; it’s called JSON.parse
.
Joe retrieves his laptop and shows Theo an example of string deserialization. The code illustrates a common usage of JSON.parse
.
var myString = "{"publicationYear":1987,"title":"Watchmen"}"; var myData = JSON.parse(myString); _.get(myData, "title"); // → "Watchmen"
THEO Cool! Let me try writing a unit test for Library.searchBooksByTitleJSON
using JSON.parse
.
It doesn’t take Theo too much time to come up with a piece of code. Using his laptop, he inputs the unit test.
var libraryData = { "catalog": { "booksByIsbn": { "978-1779501127": { "isbn": "978-1779501127", "title": "Watchmen", "publicationYear": 1987, "authorIds": ["alan-moore", "dave-gibbons"] } }, "authorsById": { "alan-moore": { "name": "Alan Moore", "bookIsbns": ["978-1779501127"] }, "dave-gibbons": { "name": "Dave Gibbons", "bookIsbns": ["978-1779501127"] } } } }; var bookInfo = { "isbn": "978-1779501127", "title": "Watchmen", "authorNames": ["Alan Moore", "Dave Gibbons"] }; _.isEqual(JSON.parse(Library.searchBooksByTitleJSON(libraryData, "Watchmen")), [bookInfo]); _.isEqual(JSON.parse(Library.searchBooksByTitleJSON(libraryData, "Batman")), []);
JOE Well done! I think you’re ready to move on to the last piece of the puzzle and write the unit test for Catalog.searchBooksByTitle
.
Because Theo and Joe have been discussing unit tests for quite some time, he asks Joe if he would like another espresso. They call the waiter and order, then Theo looks again at the code for Catalog.searchBooksByTitle
.
Catalog.searchBooksByTitle = function(catalogData, query) { var allBooks = _.get(catalogData, "booksByIsbn"); var matchingBooks = _.filter(allBooks, function(book) { return _.get(book, "title").includes(query); }); var bookInfos = _.map(matchingBooks, function(book) { return Catalog.bookInfo(catalogData, book); }); return bookInfos; };
Writing the unit test for Catalog.searchBooksByTitle
is a more pleasant experience for Theo than writing the unit test for Library.searchBooksByTitleJSON
. He appreciates this for two reasons:
It’s not necessary to deserialize the output because the function returns data.
It’s not necessary to wrap the catalog data in a library data map.
var catalogData = { "booksByIsbn": { "978-1779501127": { "isbn": "978-1779501127", "title": "Watchmen", "publicationYear": 1987, "authorIds": ["alan-moore", "dave-gibbons"] } }, "authorsById": { "alan-moore": { "name": "Alan Moore", "bookIsbns": ["978-1779501127"] }, "dave-gibbons": { "name": "Dave Gibbons", "bookIsbns": ["978-1779501127"] } } }; var bookInfo = { "isbn": "978-1779501127", "title": "Watchmen", "authorNames": ["Alan Moore", "Dave Gibbons"] }; _.isEqual(Catalog.searchBooksByTitle(catalogData, "Watchmen"), [bookInfo]); _.isEqual(Catalog.searchBooksByTitle(catalogData, "Batman"), []);
THEO I thought I was done. What did I miss?
JOE You forgot to test cases where the query string is all lowercase.
THEO You’re right! Let me quickly add one more test case.
In less than a minute, Theo creates an additional test case and shows it to Joe. What a disappointment when Theo discovers that the test case with "watchmen"
in lowercase fails!
JOE Don’t be too upset, my friend. After all, the purpose of unit tests is to find bugs in the code so that you can fix them. Can you fix the code of CatalogData.searchBooksByTitle
?
THEO Sure. All I need to do is to lowercase both the query string and the book title before comparing them. I’d probably do something like this.
Catalog.searchBooksByTitle = function(catalogData, query) { var allBooks = _.get(catalogData, "booksByIsbn"); var queryLowerCased = query.toLowerCase(); ❶ var matchingBooks = _.filter(allBooks, function(book) { return _.get(book, "title") .toLowerCase() ❷ .includes(queryLowerCased); }); var bookInfos = _.map(matchingBooks, function(book) { return Catalog.bookInfo(catalogData, book); }); return bookInfos; };
❶ Converts the query to lowercase
❷ Converts the book title to lowercase
After fixing the code of Catalog.searchBooksByTitle
, Theo runs all the test cases again. This time, all of them pass—what a relief!
JOE It’s such good feeling when all the test cases pass.
JOE I think we’ve written unit tests for all the search query code, so now we’re ready to write unit tests for mutations. Thank goodness the waiter just brought our coffee orders.
JOE Before writing unit tests for the add member mutation, let’s draw the tree of function calls for System.addMember
.
Theo takes a look at the code for the functions involved in the add member mutation. He notices the code is spread over three classes: System
, Library
, and UserManagement
.
System.addMember = function(systemState, member) { var previous = systemState.get(); var next = Library.addMember(previous, member); systemState.commit(previous, next); }; Library.addMember = function(library, member) { var currentUserManagement = _.get(library, "userManagement"); var nextUserManagement = UserManagement.addMember( currentUserManagement, member); var nextLibrary = _.set(library, "userManagement", nextUserManagement); return nextLibrary; }; UserManagement.addMember = function(userManagement, member) { var email = _.get(member, "email"); var infoPath = ["membersByEmail", email]; if(_.has(userManagement, infoPath)) { throw "Member already exists."; } var nextUserManagement = _.set(userManagement, infoPath, member); return nextUserManagement; };
Theo grabs another napkin. Drawing the tree of function calls for System.addMember
is now quite easy (see figure 6.5).
JOE Excellent! So which functions of the tree should be unit tested for the add member mutation?
THEO I think the functions we need to test are System.addMember
, SystemState .get
, SystemState.commit
, Library.addMember
, and UserManagement .addMember
. That right?
JOE You’re totally right. Let’s defer writing unit tests for functions that belong to SystemState
until later. Those are generic functions that should be tested outside the context of a specific mutation. Let’s assume for now that we’ve already written unit tests for the SystemState
class. We’re left with three functions: System.addMember
, Library.addMember
, and UserManagement.addMember
.
THEO In what order should we write the unit tests, bottom up or top down?
JOE Let’s start where the real meat is—in UserManagement.addMember
. The two other functions are just wrappers.
JOE Writing a unit test for the main function of a mutation requires more effort than writing the test for a query. The reason is that a query returns a response based on the system data, whereas a mutation computes a new state of the system based on the current state of the system and some arguments (see figure 6.6).
?Tip Writing a unit test for the main function of a mutation requires more effort than for a query.
THEO It means that in the test cases of UserManagement.addMember
, both the input and the expected output are maps that describe the state of the system.
JOE Exactly. Let’s start with the simplest case, where the initial state of the system is empty.
THEO You mean that userManagementData
passed to UserManagement.addMember
is an empty map?
Once again, Theo places his hands over his laptop keyboard, thinks for a moment, and begins typing. He reminds himself that the code needs to add a member to an empty user management map and to check that the resulting map is as expected. When he’s finished, he shows his code to Joe.
var member = { "email": "[email protected]", "password": "my-secret" }; var userManagementStateBefore = {}; var expectedUserManagementStateAfter = { "membersByEmail": { "[email protected]": { "email": "[email protected]", "password": "my-secret" } } }; var result = UserManagement.addMember(userManagementStateBefore, member); _.isEqual(result, expectedUserManagementStateAfter);
JOE Very nice! Keep going and write a test case when the initial state is not empty.
Theo knows this requires a few more lines of code but nothing complicated. When he finishes, he once again shows the code to Joe.
var jessie = { "email": "[email protected]", "password": "my-secret" }; var franck = { "email": "[email protected]", "password": "my-top-secret" }; var userManagementStateBefore = { "membersByEmail": { "[email protected]": { "email": "[email protected]", "password": "my-top-secret" } } }; var expectedUserManagementStateAfter = { "membersByEmail": { "[email protected]": { "email": "[email protected]", "password": "my-secret" }, "[email protected]": { "email": "[email protected]", "password": "my-top-secret" } } }; var result = UserManagement.addMember(userManagementStateBefore, jessie); _.isEqual(result, expectedUserManagementStateAfter);
JOE Awesome! Can you think of other test cases for UserManagement.addMember
?
JOE What about cases where the mutation fails?
THEO Right! I always forget to think about negative test cases. I assume that relates to the fact that I’m an optimistic person.
?Tip Don’t forget to include negative test cases in your unit tests.
JOE Me too. The more I meditate, the more I’m able to focus on the positive side of life. Anyway, how would you write a test case where the mutation fails?
THEO I would pass to UserManagement.addMember
a member that already exists in userManagementStateBefore
.
JOE And how would you check that the code behaves as expected in case of a failure?
THEO Let me see. When a member already exists, UserManagement.addMember
throws an exception. Therefore, what I need to do in my test case is to wrap the code in a try/catch
block.
Once again, it doesn’t require too much of an effort for Theo to create a new test case. When he’s finished, he eagerly turns his laptop to Joe.
var jessie = { "email": "[email protected]", "password": "my-secret" }; var userManagementStateBefore = { "membersByEmail": { "[email protected]": { "email": "[email protected]", "password": "my-secret" } } }; var expectedException = "Member already exists."; var exceptionInMutation; try { UserManagement.addMember(userManagementStateBefore, jessie); } catch (e) { exceptionInMutation = e; } _.isEqual(exceptionInMutation, expectedException);
THEO Now, I think I’m ready to move forward and write unit tests for Library.addMember
and System.addMember
.
JOE I agree with you. Please start with Library.addMember
.
THEO Library.addMember
is quite similar to UserManagement.addMember
. So I guess I’ll write similar test cases.
JOE In fact, that won’t be required. As I told you when we wrote unit tests for a query, when you write a unit test for a function, you can assume that the functions down the tree work as expected.
THEO Right. So I’ll just write the test case for existing members.
Theo starts with a copy-and-paste of the code from the UserManagement.addMember
test case with the existing members in listing 6.23. After a few modifications, the unit test for Library.addMember
is ready.
var jessie = { "email": "[email protected]", "password": "my-secret" }; var franck = { "email": "[email protected]", "password": "my-top-secret" }; var libraryStateBefore = { "userManagement": { "membersByEmail": { "[email protected]": { "email": "[email protected]", "password": "my-top-secret" } } } }; var expectedLibraryStateAfter = { "userManagement": { "membersByEmail": { "[email protected]": { "email": "[email protected]", "password": "my-secret" }, "[email protected]": { "email": "[email protected]", "password": "my-top-secret" } } } }; var result = Library.addMember(libraryStateBefore, jessie); _.isEqual(result, expectedLibraryStateAfter);
JOE Beautiful! Now, we’re ready for the last piece. Write a unit test for System .addMember
. Before you start, could you please describe the input and the output of System.addMember
?
Theo takes another look at the code for System.addMember
and hesitates; he’s a bit confused. The function doesn’t seem to return anything!
System.addMember = function(systemState, member) { var previous = systemState.get(); var next = Library.addMember(previous, member); systemState.commit(previous, next); };
THEO The input of System.addMember
is a system state instance and a member. But, I’m not sure what the output of System.addMember
is.
JOE In fact, System.addMember
doesn’t have any output. It belongs to this stateful part of our code that doesn’t deal with data manipulation. Although DOP allows us to reduce the size of the stateful part of our code, it still exists. Here is how I visualize it.
Joe calls the waiter to see if he can get more napkins. With that problem resolved, he draws the diagram in figure 6.7.
THEO Then how do we validate that the code works as expected?
JOE We’ll retrieve the system state after the code is executed and compare it to the expected value of the state.
THEO OK. I’ll try to write the unit test.
JOE Writing unit tests for stateful code is more complicated than for data manipulation code. It requires the calm of the office.
THEO Then let’s go back to the office. Waiter! Check, please.
Theo picks up the tab, and he and Joe take the cable car back to Albatross. When they’re back at the office, Theo starts coding the unit test for Library.addMember
.
THEO Can we use _.isEqual
with system state?
JOE Definitely. The system state is a map like any other map.
?Tip The system state is a map. Therefore, in the context of a test case, we can compare the system state after a mutation is executed to the expected system state using _.isEqual
Theo copies and pastes the code for Library.addMember
(listing 6.21), which initializes the data for the test. Then, he passes a SystemState
object that is initialized with libraryStateBefore
to System.addMember
. Finally, to complete the test, he compares the system state after the mutation is executed with the expected value of the state.
class SystemState { systemState; get() { return this.systemState; } commit(previous, next) { this.systemState = next; } } window.SystemState = SystemState;
var jessie = { "email": "[email protected]", "password": "my-secret" }; var franck = { "email": "[email protected]", "password": "my-top-secret" }; var libraryStateBefore = { "userManagement": { "membersByEmail": { "[email protected]": { "email": "[email protected]", "password": "my-top-secret" } } } }; var expectedLibraryStateAfter = { "userManagement": { "membersByEmail": { "[email protected]": { "email": "[email protected]", "password": "my-secret" }, "[email protected]": { "email": "[email protected]", "password": "my-top-secret" } } } }; var systemState = new SystemState(); ❶ systemState.commit(null, libraryStateBefore); ❷ System.addMember(systemState, jessie); ❸ _.isEqual(systemState.get(), expectedLibraryStateAfter); ❹
❶ Creates an empty SystemState object (see chapter 4)
❷ Initializes the system state with the library data before the mutation
❸ Executes the mutation on the SystemState object
❹ Validates the state after the mutation is executed
JOE Wow, I’m impressed; you did it! Congratulations!
THEO Thank you. I’m so glad that in DOP most of our code deals with data manipulation. It’s definitely more pleasant to write unit tests for stateless code that only deals with data manipulation.
JOE Now that you know the basics of DOP, would you like to refactor the code of your Klafim prototype according to DOP principles?
THEO Definitely. Nancy told me yesterday that Klafim is getting nice market traction. I’m supposed to have a meeting with her in a week or so about the next steps. Hopefully, she’ll be willing to work with Albatross for the long term.
JOE Exciting! Do you know what might influence Nancy’s decision?
THEO Our cost estimate, certainly, but I know she’s in touch with other software companies. If we come up with a competitive proposal, I think we’ll get the deal.
JOE I’m quite sure that after refactoring to DOP, features will take much less time to implement. That means you should be able to quote Nancy a lower total cost than the competition, right?
THEO I’ll keep my fingers crossed!
The meeting with Nancy went well. Albatross got the deal, Monica (Theo’s boss) is pleased, and it’s going to be a long-term project with a nice budget. They’ll need to hire a team of developers in order to meet the tough deadlines. While driving back to the office, Theo gets a phone call from Joe.
JOE How was your meeting with Nancy?
JOE Awesome! I told you that with DOP the cost estimation would be lower.
THEO In fact, we are not going to use DOP for this project.
THEO After refactoring the Library Management System prototype to DOP, I did a deep analysis with my engineers. We came to the conclusion that DOP might be a good fit for the prototype phase, but it won’t work well at scale.
JOE Could you share the details of your analysis?
THEO I can’t right now. I’m driving.
JOE Could we meet in your office later today?
THEO I’m quite busy with the new project and the tough deadlines.
JOE Let’s meet at least in order to have a proper farewell.
THEO OK. Let’s meet at 4 PM, then.
►Note The story continues in the opener of part 2.
Most of the code in a data-oriented system deals with data manipulation.
It’s straightforward to write unit tests for code that deals with data manipulation.
Test cases follow the same simple general pattern:
b) Generate expected data output
c) Compare the output of the function with the expected data output
In order to compare the output of a function with the expected data output, we need to recursively compare the two pieces of data.
The recursive comparison of two pieces of data is implemented via a generic function.
When a function returns a JSON string, we parse the string back to data so that we deal with data comparison instead of string comparison.
A tree of function calls for a function f
is a tree where the root is f
, and the children of a node g
in the tree are the functions called by g
.
The leaves of the tree are functions that are not part of the code base of the application and are functions that don’t call any other functions.
The tree of function calls visualization guides us regarding the quality and quantity of the test cases in a unit test.
Functions that appear in a lower level in the tree of function calls tend to involve less complex data than functions that appear in a higher level in the tree.
Functions that appear in a lower level in the tree of function calls usually need to be covered with more test cases than functions that appear in a higher level in the tree.
Unit tests for mutations focus on the calculation phase of the mutation.
We compare the output and the expected output of our functions with a generic function that recursively compares two pieces of data (e.g., _.isEqual
).
When we write a unit test for a function, we assume that the functions called by this function are covered by the unit tests and work as expected. This significantly reduces the quantity of test cases in our unit tests.
We avoid using string comparison in unit tests for functions that deal with data.
Writing a unit test for the main function of a mutation requires more effort than for a query.
The system state is a map. Therefore, in the context of a test case, we can compare the system state after a mutation is executed to the expected system state using a generic function like _.isEqual
.
3.133.111.85