One of the key principles of engineering secure software is having a testing mindset throughout development. Trust your own code only after testing it. This can be a real challenge in software development, as programming gives us so much freedom that it's easy to over-complicate our designs.
One of the best ways to maintain a tester's mindset as a developer is a practice called test-driven development (TDD). The idea behind TDD is to take unit testing to the next level: that everything must be unit tested, to the point where the functionality originates from getting the test to pass. At each step, we mentally and programmatically define our expectations of the system prior to writing our code. The TDD methodology looks like this:
Additionally, this activity covers an advanced topic in unit testing: mock objects. These are "fake" Java objects that we use to isolate a single class to be tested. We'll use some of the magical manipulations of the EasyMock library to create robust mocks.
Finally, the example we will be going through today is Human Resources, a simple skeleton of a program designed around Model-View-Controller. The View and the Model have not been implemented, and we we don't need to implement them today.
Download the project, and import into Eclipse.
Go to
File > Import > Existing Projects into Workspace
. Choose
Select archive
. The dialog should look like this:
Take a look at the given classes. Note that we have one model,
UserDAO
(DAO==Database Access Object), and it's completely unimplemented. But
our controller,
ManageUsers
, depends upon calls to
UserDAO
Also, take a look at the given example test
GetUserTest
. You might recognize it from lecture. Review this briefly and make
sure you understand it.
We'll need to re-run our unit tests a lot in this exercise, so let's
set up a run configuration. Right-click on the project, and go to
Run As > JUnit test
. You should get a green bar in JUnit
We'll need to tweak our Eclipse settings so we can re-launch our unit
tests quickly. Go to
Window > Preferences > Run > Launching
. On that pane, make sure that the
Always launch the previously launched application
option is checked. Close the preferences window.
Now hit
Ctrl + F11
. This re-runs our unit tests. Green bar!
Let's start our TDD with a bug fix. The
ManageUsers
controller should be allowing numbers in the userID. So, Step 1 of TDD
says that we start with a unit test. Go to
GetUsersTest
, and make a copy of the test
lookupBobby
. Change the name of the new test to
lookupBobbyTables123
.
Now, change the unit test so that we're asking the controller to look
up
BobbyTables123
. This requires a change in two places: first, in the call to
ManageUsers
- the actual input to our test. But, we also need to change our expectations
set on our mock object. Before, we told our mock
UserDAO
that it was going to get a lookup for a "BobbyTables", but now we
expect successful database call of "BobbyTables123". Thus, our unit
test should now look like this:
Run your unit tests (
Ctrl + F11
). Red bar!
This red bar is exactly what we expected, because we haven't actually
fixed the code yet. Go fix the code in the
validate
private method of
ManageUsers
. The regex should look like
[a-zA-Z0-9]*
Re-run. Green bar!
Let's go for some new functionality entirely. We will build a method
in
ManageUsers
that will allow us make a given user an admin. In particular, we
expect the controller to do three key actions:
Back to Step 1 of TDD. Create a new unit test by right-clicking on the
testing folder package and going to
New > Class
. Name the class
MakeAdminTest
, hit Finish.
Copy in our basic unit test setup for this activity. Hit
Ctrl + Shift + O
to "Organize Imports". Everything should compile.
public class MakeAdminTest { // Setup mocks private final IMocksControl ctrl = EasyMock.createControl(); private final UserDAO userDAO = ctrl.createMock(UserDAO.class); private final User mockUser = ctrl.createMock(User.class); @Before public void init() { ctrl.reset(); } @Test public void makeAdmin() throws Exception { // Establish expectations // replace this with expectations ctrl.replay(); // Run & Verify // replace this with running the test ctrl.verify(); } }
Here's a breakdown of everything you see here:
We still need establish how we expect our mocks to be called. Based on the previous description, we have three expectations: authenticate the giver, lookup the user, and make the change. Here are the first two:
expect(userDAO.canAuthenticate("adminman", "abc123")).andReturn(true).once(); expect(userDAO.find("BobbyTables")).andReturn(mockUser).once();
To make this compile, you'll need this line at the top of your test class:
import static org.easymock.EasyMock.*;
Our expectations are this: an authentication call will be made for
administrator "adminman" with password "abc123". When that call is
made, the method returns a boolean
true
. This method can only be executed once on the mock object, otherwise
the test fails. Note:
expect
is another method call from EasyMock that sets up this clever little
chain.
For the second expectation, BobbyTables is looked up in the database.
When that call is made, we return another mock object,
mockUser
. Allow that once. (Note: by making a User mock, we are
establishing the implicit expectation that none of User's
calls are to be made, since we didn't establish any.)
Now fill in the third expectation for the test: making the call to
UserDAO
that BobbyTables was made an admin.
Finally, we need to put in our call to
ManageUsers
. Add this line to the Run and Verify section.
new ManageUsers(userDAO).makeAdmin("adminman", "abc123", "BobbyTables");
After hitting
Ctrl + Shift + O
, you'll notice that we still have a compile error. We expected that,
because we haven't written our code yet! Step 2 of TDD is to write
your stubs to get it to compile. Fortunately, Eclipse's Quick Fix can
help us generate our method stubs. With your text cursor on the
makeAdmin
method, hit
Ctrl + 1
(the number one). You'll see an option to generate a method.
Eclipse figured out that we needed a void method, with three Strings. Pretty cool, huh? Rename the variables to be more meaningful. Like this:
Your unit test should now compile. Run it. Red bar!
Note that this is what failing expectations in EasyMock look like. It expected three calls, it got none.
On to Step 3 of TDD. Fill in the functionality of the
makeAdmin
call, based on our tests and the previous description.
When that new functionality is done, green bar!
Now let's say that the
makeAdmin
method needs a little bit of refactoring. Place the text cursor over
the declaration of
makeAdmin
. Go to
Refactor > Change Method Signature...
Instead of the order of parameters being (
adminUser, password, userToBeAdmin
) , change the order to: (
userToBeAdmin, adminUser, password
) . Like this:
Hit Ok and re-run your unit tests. Green bar!
Now it's time to attempt TDD with mock objects on your own. You have four separate tasks, in increasingly difficult order:
isAdmin(String)
to
UserDAO
add(User user)
to
UserDAO
ManageUsers
called addUser(String
username)
User
object, which our test can't
access. You will need EasyMock's
anyObject(Class<T> clazz)
Due 4-2. Zip up your Eclipse project and submit it to the dropbox on myCourses. This activity is worth 30 points and is graded individually. Grading breakdown is as follows: