|
Building project with Ant
|
Let's make Ant script for our project to simplify compilation and tests running process.
And run our test at last! As soon as you have the structure of project as it is
shown on right image,
initial script have to search for libraries in "lib" folder, all sources are available
from "src", "src/test" contains sources of your test cases and "src/main" contains
main application code. To make our tests work we should teach Ant to call
compiler and test runner. Create build.xml file right in JavaTesting directory and copy
there the script listed below:
|
|
<project name="JavaTesting" default="help">
<target name="help">
<echo>ant compile: compile the project</echo>
<echo>ant junit: run unit tests</echo>
<echo>ant fit: run fit tests</echo>
</target>
<property name="src" location="src" />
<property name="src.test" location="src/test" />
<property name="fit" location="fit" />
<property name="bin" location="bin" />
<property name="lib" location="lib" />
<property name="report" location="report" />
<property name="junit.report" location="report/junit" />
<property name="fit.report" location="report/fit" />
<path id="lib.classpath">
<fileset dir="${lib}">
<include name="**/*.jar" />
</fileset>
</path>
<path id="test.classpath">
<path refid="lib.classpath" />
<pathelement location="${bin}" />
</path>
<target name="compile">
<mkdir dir="${bin}" />
<javac srcdir="${src}" destdir="${bin}">
<classpath refid="lib.classpath" />
</javac>
</target>
<target name="junit" depends="compile">
<mkdir dir="${junit.report}" />
<junit printsummary="yes" fork="yes" haltonfailure="no">
<classpath refid="test.classpath" />
<formatter type="plain" />
<batchtest fork="yes" todir="${junit.report}">
<fileset dir="${src.test}">
<include name="**/*Test.java" />
</fileset>
</batchtest>
</junit>
</target>
<target name="fit" depends="compile">
<mkdir dir="${fit.report}" />
<java classname="fitlibrary.runner.FolderRunner" fork="yes"
failonerror="yes">
<classpath refid="test.classpath" />
<arg value="${fit}" />
<arg value="${fit.report}" />
</java>
<echo>Please see the FIT report page for more details</echo>
</target>
<target name="clean">
<delete dir="${bin}" />
<delete dir="${report}" />
</target>
</project>
Running ahead FIT framework will be highlighted later in this article but
there is nothing worse if we include task for fitlibraryRunner right now.
Structure of build.xml is very simple. Tag project contains several targets
for compiling code and running tests of two different kinds: unit and
integration. Default target prints options for running other targets. Run
this target from JavaTesting/ directory to see how it works:
ant
It produces the next output:
Buildfile: build.xml
help:
[echo] ant compile: compile the project
[echo] ant junit: run unit tests
[echo] ant fit: run fit tests
BUILD SUCCESSFUL
Total time: 0 seconds
As soon as "junit" target depends on "compile" execute command
ant junit
Ant will create folder "bin", compile source code and run ScoreCalculatorImplTest.
So if you see the next output, your tests have been executed successfully!
Buildfile: build.xml
compile:
[javac] Compiling 6 source files to
D:\java_projects\JavaTesting\bin
junit:
[junit] Running
com.intelrate.examination.score.ScoreCalculatorImplTest
[junit] Tests run: 5,
Failures: 0,
Errors: 0,
Time elapsed: 0,078 sec
BUILD SUCCESSFUL
Total time: 4 seconds
Detailed report with information about executed tests is placed into report/junit/
What benefits we have so far
As you see, existed test case is bigger than tested class. Process "code a little
- test a little" has been under my observation during a long period of time. And often
team ignoring tests spend three times less resources at initial stage.

But it is going on only during a few first iterations. While your project is getting complex
you are spending more and more time on manual testing and debugging and fixing
bugs, debugging and manual testing again... This process is described by
diagram you see. Green line shows costs of maintaining code with automated tests
and red line growing exponentially shows maintaining costs of manually tested system.
Maintaining of code covered with tests is less expensive not
only because of manual testing reducing. Automated tests facilitates
changes. It means that you are able to provide as much refactoring as you want
and tests will say you if you do something wrong. It can really increase not only speed
of development but also quality of your product. As we see JUnit increases productivity
in long-term outlook.
As we determined earlier, all the code which can be broken should be covered with tests.
Do tests extirpate all our possible faults? I tried to run previous test case using my
favorite code coverage tool and found that all the code in ScoreCalculator is covered.
It meens that all the instructions are execuded when test runs. But it is not enough.
You also should always provide qualitative and detailed asserts to be sure your code
works as you expect. It seems that I've done all such an asserts in tests for ScoreCalculator.
Though your test controls much, it doesn't control all. Just add the next test:
public void testCalculateScoreMax2()
{
final Boolean[] testResults = {true, true, true, true, true, true, true};
final Float expectedScore = ScoreCalculator.MAX_SCORE;
final Float score = scoreCalculator.calculateScore(testResults);
assertEquals(expectedScore, score);
}
Go to your project root folder and run
ant junit
You will get the next output:
Buildfile: build.xml
compile:
[javac] Compiling 6 source files to D:\java_projects\JavaTesting\bin
junit:
[junit] Running com.intelrate.examination.score.ScoreCalculatorImplTest
[junit] Tests run: 6, Failures: 1, Errors: 0, Time elapsed: 0,11 sec
BUILD SUCCESSFUL
Total time: 4 seconds
Pay attention to number of failures. If you investigate the report you will find
that testCalculateScoreMax2 failed with the message: expected:<7.0> but was:<7.0000005>.
It is really usefull information because now you see that residue of division affected
the result. Our calculator works incorrectly. But it doesn't matter. I only tried to show
that even the best test can't be perfect. There is no silver bullet
JMock: magic or craftsmanship
Our code is growing up. Add StudentImpl.java class implementing Student interface:
package com.intelrate.examination.student.impl;
import java.util.HashMap;
import java.util.Map;
import com.intelrate.examination.qualification.Qualification;
import com.intelrate.examination.student.Student;
import com.intelrate.examination.test.Test;
public class StudentImpl implements Student
{
final Map bestScores = new HashMap();
/**
* {@inheritDoc}
*/
public boolean confirmQualification(final Qualification qualification)
{
final Map requiredScores = qualification.getRequirements();
for (Test test : requiredScores.keySet())
{
if (bestScores.get(test) == null
|| bestScores.get(test) < requiredScores.get(test))
{
return false;
}
}
return true;
}
/**
* {@inheritDoc}
*/
public void updateTestScore(final Test test, final Float score)
{
final Float bestTestScore = bestScores.get(test);
if (bestTestScore == null || bestTestScore < score)
{
bestScores.put(test, score);
}
}
}
Method updateTestScore checks if score for the given test doesn't exist or less then
new score and updates it in that case. Qualification confirmation is more complex.
It verifies that student has all required tests passed with appropriate scores.
There are several ways to cover StudentImpl class with tests. We can use JUnit and
existed ScoreCalculator implementation. But in that case our test will control too
many classes. And if it fails we will have to debug code trying to find fault. If
we warranty that all used classes satisfy contracts declared in interfaces we will
know that test fails because of mistake in tested class or method. And often you
will determine the cause of break next moment after your test failed.
It is gladness that we have instrument to realize our intention. First create test
case StudentImplTest.java and then we will discuss how it works.
package com.intelrate.examination.student;
import java.util.HashMap;
import java.util.Map;
import junit.framework.TestCase;
import org.jmock.Expectations;
import org.jmock.Mockery;
import com.intelrate.examination.qualification.Qualification;
import com.intelrate.examination.student.impl.StudentImpl;
import com.intelrate.examination.test.Test;
public class StudentImplTest extends TestCase
{
Mockery context = new Mockery();
Student student;
Qualification mockQualification;
Map requiredScores;
private final static Float GOOD_SCORE = 5.0F;
private final static Float EXCELLENT_SCORE = 6.0F;
private final static Float PERFECT_SCORE = 7.0F;
public void setUp()
{
student = new StudentImpl();
mockQualification = context.mock(Qualification.class);
requiredScores = new HashMap();
requiredScores.put(Test.CPP, EXCELLENT_SCORE);
requiredScores.put(Test.JAVA, GOOD_SCORE);
context.checking(new Expectations() {{
one(mockQualification).getRequirements();
will(returnValue(requiredScores));
}});
}
public void testConfirmQualificationSuccessfully1()
{
student.updateTestScore(Test.CPP, PERFECT_SCORE);
student.updateTestScore(Test.JAVA, GOOD_SCORE);
assertTrue(student.confirmQualification(mockQualification));
}
public void testConfirmQualificationSuccessfully2()
{
student.updateTestScore(Test.CPP, GOOD_SCORE);
student.updateTestScore(Test.JAVA, GOOD_SCORE);
assertFalse(student.confirmQualification(mockQualification));
student.updateTestScore(Test.CPP, PERFECT_SCORE);
context.checking(new Expectations() {{
one(mockQualification).getRequirements();
will(returnValue(requiredScores));
}});
assertTrue(student.confirmQualification(mockQualification));
}
public void testConfirmQualificationFailed1()
{
student.updateTestScore(Test.CPP, PERFECT_SCORE);
assertFalse(student.confirmQualification(mockQualification));
}
public void testConfirmQualificationFailed2()
{
student.updateTestScore(Test.CPP, GOOD_SCORE);
student.updateTestScore(Test.JAVA, GOOD_SCORE);
assertFalse(student.confirmQualification(mockQualification));
}
}
setUp prepares student instance to be tested and mock object of Qualification.
We define our expectations and behaviour of meta objects so that we can
define consequences of using interfaces by tested class. I believe that code
speaks for itself. Tests are not in want of describing too. Student has several
tests passed with the given scores and we check that he deserves predefined
qualification or not. Only the notice is that you should always update your
expectations as it is done in the second test.
JMock is very powerful framework. And it is very important to specify in test
exactly what you want to happen and no more. Especially when you have some code
implemented. Tests have to show how code should work but not how it works in reality.
Brittle tests may apply the brake to development speed since they inhibit refactoring.
Tests with mock objects allow you to clarify the interactions between classes
and as a result to reduce dependencies. The effect is really magical. Especially
in case of test-driven development. Tests with JMock have good influence on design:
interfaces become more modularized, flexible, and extensible. For example anti-pattern
"high cohesion" is cleaning out very effectively by tests with JMock. The symptom
is code like man.getBody().getHead().getLips().smile(); which collects dependency on
four interfaces in one place. It is hard to create four mock objects so you will
prefer to write code like man().feelHappy(); and design will be improved. But you
should always understand what is going on. Because it is not too hard to make
useless tests.
As we discussed it earlier one of the most useful benefits of unit tests
is opportunity to make refactoring without additional expenses.
Maintaining of automated tests can be very expensive if many tests fail when
you change anything in one class or even method.
JMock allows you to avoid such a problem. Run your tests again if you
haven't done it and prepare to dive into integration testing.
FIT: Framework for Integrated Test
Sometimes making our system we don't catch sight of its behaviour from user's point
of view. We should care about business logic during development process
if we want to obtain success at the end of iteration. FIT helps us to abstract
one's mind from implementation details and even desing. Let us see the next
example to understand this statement:
Examination Process Test
Agenda: Developer and Software Architect qualifications have
required tests to be passed and minimal score for every test. To earn qualification student
James Bond logins to the examination system and passes several tests. After that we are able
to ask him about his qualifications.
|
com.intelrate.examination.fit.ExaminationProcessFixture
|
| set up requirements for |
developer |
| test |
score |
| CPP |
5.0 |
| JAVA |
4.5 |
| set up requirements for |
software architect |
| test |
score |
| CPP |
6.0 |
| JAVA |
6.0 |
| OOD |
5.0 |
| login |
James Bond |
password |
007 |
| pass test |
CPP |
with results |
true, false, true, false, true, true |
| pass test |
JAVA |
with results |
false, true, true, true, true, false |
| qualification |
developer |
confirmed |
true |
| qualification |
software architect |
confirmed |
false |
First copy this file to JavaTesting/fit/
ExaminationProcessTest.html.
Just save it as target if you have no one HTML editor at hand. The next
step of making FIT test consists in providing special Fixtures
linking up HTML test with your source code. This sample is served
by two fixtures. Add first of them to src/test/com/intelrate/examination/fit
under the name ExaminationProcessFixture.java
package com.intelrate.examination.fit;
import org.jmock.Expectations;
import org.jmock.Mockery;
import com.intelrate.examination.qualification.Qualification;
import com.intelrate.examination.score.ScoreCalculator;
import com.intelrate.examination.score.impl.ScoreCalculatorImpl;
import com.intelrate.examination.student.Student;
import com.intelrate.examination.student.impl.StudentImpl;
import com.intelrate.examination.test.Test;
import fitlibrary.DoFixture;
public class ExaminationProcessFixture extends DoFixture
{
Mockery context = new Mockery();
final Qualification mockQualification = context.mock(Qualification.class);
final ScoreCalculator scoreCalculator = new ScoreCalculatorImpl();
Student student;
public boolean loginPassword(final String login, final String password)
{
student = new StudentImpl();
/// TODO: Implement logging here when it will be ready
return true;
}
public ExaminationSetUpFixture setUpRequirementsFor(
final String qualificationName)
{
return new ExaminationSetUpFixture(qualificationName);
}
public boolean passTestWithResults(final String test, final Boolean[] results)
{
student.updateTestScore(Test.valueOf(test),
scoreCalculator.calculateScore(results));
return true;
}
public boolean qualificationConfirmed(final String qualification,
final Boolean confirmed)
{
context.checking(new Expectations() {{
one(mockQualification).getRequirements();
will(returnValue(
ExaminationSetUpFixture.getRequirements(qualification)));
}});
return confirmed.equals(student.confirmQualification(mockQualification));
}
}
As you see, this fixture implements all calls from FIT test.
If method returns true it means that test has been
passed successfully. It is handy to create special separated
fixture to prepare test data. Method setUpRequirementsFor
returns such a fixture. And as soon as instance of this
fixture is created, FIT switches context to set up fixture
which collets calls "test - score". Create file
src/test/com/intelrate/examination/fit/ExaminationSetUpFixture.java
and copy there code implementing accumulation of test data:
package com.intelrate.examination.fit;
import java.util.HashMap;
import java.util.Map;
import com.intelrate.examination.test.Test;
import fitlibrary.SetUpFixture;
public class ExaminationSetUpFixture extends SetUpFixture
{
private final static Map<String, Map<Test, Float>> QUALIFICATIONS;
private final String qualification;
static
{
QUALIFICATIONS = new HashMap<String, Map<Test, Float>>();
}
public ExaminationSetUpFixture(final String qualification)
{
QUALIFICATIONS.put(qualification, new HashMap<Test, Float>());
this.qualification = qualification;
}
public boolean testScore(final String test, final Float score)
{
QUALIFICATIONS.get(qualification).put(Test.valueOf(test), score);
return true;
}
public static Map<Test, Float> getRequirements(final String qualification)
{
return QUALIFICATIONS.get(qualification);
}
}
And that's it! Try how it works and feel the power of FIT:
ant fit
It should generate report into report/fit folder.
Good time to discuss benefits given to you by FIT. What we have
really helpful in terms of our sample project? The first one
thing is that you see how your system works before you have
implemented all its components. Again it allows you to not
only check faultlessness of your classes but also find
disadvantages and weaknesses of you program from user's
point of view. For example in our test it makes sense for
student to login before passing tests so that you can realize
that you forgot to provide such a useful method. You can
write "TODO:" comment and just put up the stub as it is done
in ExaminationProcessFixture.loginPassword.
The other benefit is better documenting of your code.
Everyone know that good tests show very well how
classes work so that we can read JUnit code instead of
documentation. Usually documentation becomes obsolete
very quickly especially if it is not Java Doc and it is hard
to verify and control without reading of source code. But
not everyone can read JUnit. Test logic described by HTML is
widely available. Customers will see your progress and as you
know transparency have positive influence on their confidence.
Show them report generated by FIT and changing of requirements
will have less cost because you determine it earlier.
FIT is really powerful framework. But it is very simple at
the same time. Even junior tester is able to create new
FIT tests using existed fixtures. Write common
fixtures presenting thin layer between you tests and
functionality. It will allow to make different complex
integration tests easily.
Of course this article is just introduction to automated testing.
But I hope you to find ideas given here reasonable. So the last
my advice is to follow links listed below to learn all the details
of technologies you consider really effective.
|