Welcome!

Java Authors: Liz McMillan, Walter H. Pinson, III, Maureen O'Gara, Yakov Werde, Tony Bishop

Related Topics: Java

Java: Article

Java Product Review: What to Do If Your Code Has No Tests

Tools that practically write tests by themselves

Focusing on the execute method, only two of the four result partitions would ever happen, so we mark the global forwards "registration," "logoff," and "welcome" as unexpected. For a normal return, only the observation "@RETURN!=!null" seems like a worthy assertion.

However, all trials returned logon and none succeeded. Looking at the code, we can see that a User object isn't being found, and so all the trials are being returned to the logon outcome. This makes perfect sense; we just have to provide a valid User object to some of the trials as we saw done in the webinar. In the webinar, an omniscient architect had already provided a session factory that created a User object. This being real life, we'll have to roll our own.

The execute method calls a helper to fetch the User object. Looking at the code coverage panel for the getUser helper method, we can see that the database parameter is usually null, and when it's not, the User object is null. As a result, the code always returns a message, and the execute method always forwards to logon instead of success.

Hmmm. Why is the database parameter ever not null? We haven't set up a session factory for it and the methods that could read it from a file have been disabled. What gives?

True, right now, when we call getUser from the execute method, the database parameter would always be null, but Agitator also tests each method in isolation. When we ran a full Agitation against the code base, Agitator also called getUser itself, and used its own default factory to create the database parameter. When Agitator called getUser, it mixed up the calls null with default instantiations of MemoryUserDase and TestUserDatabase. We didn't tell Agitator to do that. This is the automatic default behavior we get out of the box.

We already have a TestUserDatabase object that exercises exception handling. To that we can add a static getTestUserDatabase method that will return an instance of TestUserDatabase populated with User and Subscription objects. Creating the method means a bit more coding, but not so much, and now we'll be able to test the product independently of the XML document parsing.

Once the method is coded, we can right-click on the Factory Assignment view, choose "Assign a Factory form a Constructor or Method," and specify our new method. The code under test does validate that the database is not null, so we should pass some nulls to exercise that code. A simple solution is to leave the original factory running, but specify a weight of 1, and change the weight on our own factory to 9. This way, we will get some nulls and empty databases, but most often we will get a copy of our seed database.

After Agitating, we have more coverage, but we're still ending up with a null User on every trial. Checking the Snapshot, it's easy to see that the username and password parameters need factories too. When we right-click on the factory assignment for username, this time we're offered the chance to "Assign a list of values" to our string parameter. The values item leads directly to the Factory Settings dialog, where we can quickly add a couple of usernames. We repeat the process for password, so that the first items on each list are valid username and password credentials. The factories will mix these entries up, so that we get a spread of valid and invalid logon attempts. Since the code doesn't validate the username and password variables, we disable the default factories by setting the weight to zero.

With these changes, we now have 100% coverage on the getUser helper, but from the Outcomes view we can see that some trials are still throwing exceptions. Checking the Observations, we see that every time an Exception is thrown errors is null. The API contract for this sub-routine implies that errors should never be null.

We make that change to the default setting for the errors parameter and the unexpected Exceptions disappear. To seal the contract, we promote the Observation "@PRE(errors.messages) != null" to an Assertion. Now if the contract is ever broken due to future code modifications, Agitator will mark the getUser method with a red mark and exclaim "errors == null."

If we Agitate the LogonAction execute method, we see that getUser is still returning errors each time. Of course this is because, in this case, the database, username, and password values are still coming from the default factories. As shown in the webinar, we promote our new factories to package scope so that we can use them with other methods.

Under the Factory Assignments for LogonExecute, we find that the Expert has correctly wired a LogonForm factory to the "form" parameter. Problem is we need the factory to include both correct and incorrect credentials. After some trial and error, we manage to replace the default factories with new Form Property Factories that return usernames and passwords from the lists we created for getUser and promoted to package scope.

Note: When creating factories be sure to scroll down and review all the available fields. For example, the last field on the Form Property Factory form is classname, which must be set to org.apache.struts.config.FormPropertyConfig.

One red mark remains. Agitator noticed that the getUser helper can throw an ExpiredPasswordException, but none of our trials threw that exception. As it happens, the TestUserDatabase class is designed to thrown that exception if a certain username value is passed, so all we need to do is add that magic value to our factory. One last Agitation, and LogonAction is clear. The code coverage for the class is not 100% mainly because the logging statements aren't being reached, but all the domain code is being exercised.

Using the Agitator, we've gone from a standing start to having solid code coverage and a series of tests. We haven't written any JUnit code, but with Agitator's assertions we have a set of test assets that will automatically evolve as code is updated. Looking around in the manuals, it becomes clear that if somebody decided to write JUnit tests, those would work in tandem with Agitators and show up in the same summary statistics.

Agitator isn't a bad way to answer the question, "How are we going to create tests for our code base?"

More Stories By Ted Husted

Ted Husted (http://husted.com/ted/) is a software engineer and an active member of several open source projects hosted by the Apache Software Foundation, including Struts and iBATIS. His books include JUnit in Action, Struts in Action, and Professional JSP Site Design. Ted is also a consultant for Agitar Software, Inc.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.