📜 ⬆️ ⬇️

Testing in Java. Junit


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 }); //... } protected void tearDown() throws Exception { toHexStringData.clear(); } public void testToHexString() { for (Iterator iterator = toHexStringData.keySet().iterator(); iterator.hasNext();) { final String expected = (String) iterator.next(); final byte[] testData = (byte[]) toHexStringData.get(expected); final String actual = StringUtils.toHexString(testData); assertEquals(expected, actual); } } //... } 

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 }); //... } @After public static void tearDownToHexStringData() { toHexStringData.clear(); } @Test public void testToHexString() { for (Map.Entry<String, byte[]> entry : toHexStringData.entrySet()) { final byte[] testData = entry.getValue(); final String expected = entry.getKey(); final String actual = StringUtils.toHexString(testData); assertEquals(expected, actual); } } } 

What do we see here?

  @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 { //... @Category(Unit.class) @Test public void testIsEmpty() { //... } //... } @RunWith(Categories.class) @Categories.IncludeCategory(Unit.class) @Suite.SuiteClasses( { OtherJUnit4Test.class, StringUtilsJUnit4CategoriesTest.class }) public class JUnit4TestSuite { } 

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


Source: https://habr.com/ru/post/120101/


All Articles