
Today,
test-driven development (TDD) , a software development technique, is becoming increasingly popular, in which a test is first written for a specific functionality, and then the implementation of this functionality is written. In practice, everything, of course, is not so perfect, but as a result, the code is not only written and tested, but the tests implicitly set the requirements for the functionality, and also show an example of using this functionality.
So, the technique is pretty clear, but the question arises, what to use to write these same tests? In this and other articles, I would like to share my experience in using various tools and techniques for testing code in Java.
Well, I'll start with, perhaps, the most famous, and therefore the most used framework for testing -
JUnit . It is used in two versions of JUnit 3 and JUnit 4. I will consider both versions, since the old projects still use the 3rd, which supports Java 1.4.
')
I do not pretend to the author of any original ideas, and perhaps many things that will be discussed in the article are familiar. But if you are still interested, then welcome under cat.
JUnit 3
To create a test, you need to inherit the test class from
TestCase , override the
setUp and
tearDown methods if necessary, and most importantly, create test methods (must start with
test ). When a test is run, an instance of the test class is first created (for each test in the class a separate instance of the class), then the
setUp method is
executed , the test itself is started, and finally the
tearDown method is
executed . If any method throws an exception, the test is considered failed.
Note: test methods must be
public void , may be
static .
The tests themselves consist of executing some code and checks. Checks are most often performed using the
Assert class, although sometimes the
assert keyword is used.
Consider an example. There is a utility for working with strings, there are methods for checking the empty string and presenting a sequence of bytes as a hexadecimal string:
public abstract class StringUtils { private static final int HI_BYTE_MASK = 0xf0; private static final int LOW_BYTE_MASK = 0x0f; private static final char[] HEX_SYMBOLS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', }; public static boolean isEmpty(final CharSequence sequence) { return sequence == null || sequence.length() <= 0; } public static String toHexString(final byte[] data) { final StringBuffer builder = new StringBuffer(2 * data.length); for (byte item : data) { builder.append(HEX_SYMBOLS[(HI_BYTE_MASK & item) >>> 4]); builder.append(HEX_SYMBOLS[(LOW_BYTE_MASK & item)]); } return builder.toString(); } }
We write tests for it using JUnit 3. In my opinion, the most convenient way is to write tests, considering the ney class as a black box, write a separate test for each significant method in this class, for each set of input parameters some expected result. For example, a test for the
isEmpty method:
public void testIsEmpty() { boolean actual = StringUtils.isEmpty(null); assertTrue(actual); actual = StringUtils.isEmpty(""); assertTrue(actual); actual = StringUtils.isEmpty(" "); assertFalse(actual); actual = StringUtils.isEmpty("some string"); assertFalse(actual); }
You can separate the data and logic of the test by transferring data creation to the
setUp method:
public class StringUtilsJUnit3Test extends TestCase { private final Map toHexStringData = new HashMap(); protected void setUp() throws Exception { toHexStringData.put("", new byte[0]); toHexStringData.put("01020d112d7f", new byte[] { 1, 2, 13, 17, 45, 127 }); toHexStringData.put("00fff21180", new byte[] { 0, -1, -14, 17, -128 });
Additional features
In addition to what has been described, there are a few additional features. For example, you can group tests. To do this, use the
TestSuite class:
public class StringUtilsJUnit3TestSuite extends TestSuite { public StringUtilsJUnit3TestSuite() { addTestSuite(StringUtilsJUnit3Test.class); addTestSuite(OtherTest1.class); addTestSuite(OtherTest2.class); } }
You can run the same test several times. To do this, use
RepeatedTest :
public class StringUtilsJUnit3RepeatedTest extends RepeatedTest { public StringUtilsJUnit3RepeatedTest() { super(new StringUtilsJUnit3Test(), 100); } }
Inheriting the test class from
ExceptionTestCase , you can check for throwing an exception:
public class StringUtilsJUnit3ExceptionTest extends ExceptionTestCase { public StringUtilsJUnit3ExceptionTest(final String name) { super(name, NullPointerException.class); } public void testToHexString() { StringUtils.toHexString(null); } }
As you can see from the examples, everything is quite simple, nothing superfluous, the minimum necessary for testing (although some necessary things are lacking).
JUnit 4
Support for new features from Java 5 has been added here; tests can now be announced using annotations. At the same time, there is backward compatibility with the previous version of the framework, almost all the examples discussed above will work here (with the exception of
RepeatedTest , it is not in the new version).
So what has changed?
Basic annotations
Consider the same example, but using new features:
public class StringUtilsJUnit4Test extends Assert { private final Map<String, byte[]> toHexStringData = new HashMap<String, byte[]>(); @Before public static void setUpToHexStringData() { toHexStringData.put("", new byte[0]); toHexStringData.put("01020d112d7f", new byte[] { 1, 2, 13, 17, 45, 127 }); toHexStringData.put("00fff21180", new byte[] { 0, -1, -14, 17, -128 });
What do we see here?
- To simplify the work, I prefer to inherit from the Assert class, although this is optional.
- Before the annotation indicates the methods that will be called before the test is executed, the methods must be public void . Here presets are usually placed for the test, in our case it is the generation of test data ( setUpToHexStringData method).
- The @BeforeClass annotation refers to the methods that will be called before creating an instance of the test class; the methods must be public static void . It makes sense to place presets for a test in the case where a class contains several tests using different presets, or when several tests use the same data in order not to waste time creating them for each test.
- After the annotation indicates the methods that will be called after the test execution, the methods must be public void . This is where the resource release operations are placed after the test, in our case, the test data clearing (the tearDownToHexStringData method).
- The @AfterClass annotation is connected with @BeforeClass , but it performs methods after the test, as in the case of @BeforeClass , the methods must be public static void .
- Test annotation refers to test methods. As before, these methods must be public void . Here are the checks themselves. In addition, this annotation has two parameters, expected - sets the expected exception and timeout - sets the time after which the test is considered failed.
@Test(expected = NullPointerException.class) public void testToHexStringWrong() { StringUtils.toHexString(null); } @Test(timeout = 1000) public void infinity() { while (true); }
If any test for any serious reason needs to be turned off (for example, this test constantly falls, but its correction is postponed until a bright future), you can zannotirovat
@Ignore . Also, if you place this annotation on a class, all tests in this class will be disabled.
@Ignore @Test(timeout = 1000) public void infinity() { while (true); }
rules
In addition to all of the above, there is a rather interesting thing - the rules. Rules are a kind of utility for tests that add functionality before and after the test.
For example, there are built-in rules for setting a timeout for a test (
Timeout ), for specifying expected exceptions (
ExpectedException ), for working with temporary files (
TemporaryFolder ), etc. To declare a rule, you need to create a
public non-
static field of the type derived from
MethodRule and annotate it using
Rule .
public class OtherJUnit4Test { @Rule public final TemporaryFolder folder = new TemporaryFolder(); @Rule public final Timeout timeout = new Timeout(1000); @Rule public final ExpectedException thrown = ExpectedException.none(); @Ignore @Test public void anotherInfinity() { while (true); } @Test public void testFileWriting() throws IOException { final File log = folder.newFile("debug.log"); final FileWriter logWriter = new FileWriter(log); logWriter.append("Hello, "); logWriter.append("World!!!"); logWriter.flush(); logWriter.close(); } @Test public void testExpectedException() throws IOException { thrown.expect(NullPointerException.class); StringUtils.toHexString(null); } }
Also on the network you can find other uses. For example,
here the possibility of a parallel test run
is considered.
Launch
But the possibilities of the framework don't end there. The way the test is run can also be configured using
@RunWith . In this case, the class specified in the annotation must be inherited from
Runner . Consider the launchers that come bundled with the framework itself.
JUnit4 - the default launch, as the name implies, is designed to run JUnit 4 tests.
JUnit38ClassRunner is designed to run tests written using JUnit 3.
SuiteMethod or
AllTests are also designed to run JUnit 3 tests. Unlike the previous launcher, a class with a static suite method that returns a test (a sequence of all tests) is passed to this class.
The suite is equivalent to the previous one, only for JUnit 4 tests. To customize the tests run, use the
@SuiteClasses annotation.
@Suite.SuiteClasses( { OtherJUnit4Test.class, StringUtilsJUnit4Test.class }) @RunWith(Suite.class) public class JUnit4TestSuite { }
Enclosed is the same as the previous version, but instead of setting using annotation all inner classes are used.
Categories - an attempt to organize tests in categories (groups). To do this, the tests are
assigned a category using
@Category , then the run test categories in the suite are configured. It might look like this:
public class StringUtilsJUnit4CategoriesTest extends Assert {
Parameterized is a rather interesting start-up; it allows you to write parameterized tests. To do this, a static method is declared in the test class that returns a list of data that will then be used as arguments to the class constructor.
@RunWith(Parameterized.class) public class StringUtilsJUnit4ParameterizedTest extends Assert { private final CharSequence testData; private final boolean expected; public StringUtilsJUnit4ParameterizedTest(final CharSequence testData, final boolean expected) { this.testData = testData; this.expected = expected; } @Test public void testIsEmpty() { final boolean actual = StringUtils.isEmpty(testData); assertEquals(expected, actual); } @Parameterized.Parameters public static List<Object[]> isEmptyData() { return Arrays.asList(new Object[][] { { null, true }, { "", true }, { " ", false }, { "some string", false }, }); } }
Theories is somewhat similar to the previous one, but it parameterizes the test method, not the constructor. The data is marked with
@DataPoints and
@DataPoint , the test method is marked with
Theory . A test using this functionality will look something like this:
@RunWith(Theories.class) public class StringUtilsJUnit4TheoryTest extends Assert { @DataPoints public static Object[][] isEmptyData = new Object[][] { { "", true }, { " ", false }, { "some string", false }, }; @DataPoint public static Object[] nullData = new Object[] { null, true }; @Theory public void testEmpty(final Object... testData) { final boolean actual = StringUtils.isEmpty((CharSequence) testData[0]); assertEquals(testData[1], actual); } }
As with the rules, other uses can be found online. For example,
here we consider the same possibility of running the test in parallel, but using the launcher.
Conclusion
This, of course, is not all that could be said about JUnit, but I tried to be brief and to the point. As you can see, the framework is quite simple to use, there are few additional features, but there is a possibility of expansion with the help of rules and startups. But despite all this, I still prefer
TestNG with its powerful functionality, which I will discuss in the next article.
Examples can be found on the
githaba .
Literature