© Adam L. Davis 2020
A. L. DavisModern Programming Made Easyhttps://doi.org/10.1007/978-1-4842-5569-8_14

14. Testing

Adam L. Davis1 
(1)
Oviedo, FL, USA
 

Testing is a very important part of the software creation process. Without automated tests, it’s very easy for bugs to creep into software.

In fact, some go as far as to say that you should write tests before you write the code. This is called TDD (test-driven development).

There are multiple test frameworks and tools to help you test your code. This book will cover some of these tools, JUnit and Spock.

Types of Tests

The following are different types of tests you might write:
  • Unit test: Test conducted on a single API call or some isolated code or component

  • Integration test: Test of a larger system that integrates together two or more components

  • Acceptance test: High-level test that matches the business requirements

  • Compatibility: Making sure that things work together

  • Functionality: Ensuring that stuff works

  • Black box: Test conducted without knowing/thinking about what’s going on in the code

  • White box: Tests written with the inside of code in mind

  • Gray box: Hybrid of black and white box testing

  • Regression: Creating a test after finding a bug, to ensure that the bug does not reappear

  • Smoke: A huge sampling of data use during a test

  • Load/stress/performance: How the system handles load (a lot of traffic to a web site for example)

The type and number of tests you write vary, based on a number of factors. The simpler a piece of code is, the less testing it requires. For example, a getter or setter does not require a test by itself.

JUnit

JUnit1 is a simple framework to write repeatable tests.

A typical JUnit test consists of multiple methods annotated with the @Test annotation.

At the top of every JUnit test class, you should include the following imports:
1   import static org.junit.jupiter.api.Assertions.*;
2   import org.junit.jupiter.api.Test;
3   import org.junit.jupiter.api.BeforeEach;
4   import org.junit.jupiter.api.AfterEach;

Use @BeforeEach to annotate initialization methods that are run before every test and @AfterEach to annotate breakdown methods that are run after every test. The methods should ensure that each test is independent.

Each test method should test one thing, and the method name should reflect the purpose of the test. For example:
1   @Test
2   public void mapSizeIsCorrect() {
3
   Map<String,String> map = new HashMap<>();
4        map.put("Smaug", "deadly");
5        map.put("Norbert", "cute");
6        assertEquals(2, map.size());
7   }

The first parameter to the assertEquals method is what is expected, and the second parameter is the actual value to test. When the two values are not equal, this will throw an Exception and the test will fail. A failing test means our expectations were not met by the code. The software should be considered incorrect at this point and not go any further until the test is successful (not failing).

Hamcrest

You can create more readable tests using the Hamcrest core matchers. In JUnit you must import Hamcrest2 matchers separately. You can import them as follows:
1  import static org.hamcrest.CoreMatchers.equalTo;
2  import static org.hamcrest.CoreMatchers.is;
3  import static org.hamcrest.MatcherAssert.assertThat;
Here’s an example of using a Hamcrest matcher:
1   @Test
2   public void sizeIs10() {
3            assertThat(map.size(), is(2));
4   }

This test would assert that map’s size is 2. There are many other matchers and you can even build your own.

Assumptions

Often, there are variables outside of a test that are beyond your control but which your test assumes to be true. When an assumption fails, it shouldn’t necessarily mean that your test fails. For this purpose, JUnit added Assumptions, which you may import like so:
1   import static org.junit.jupiter.api.Assumptions.*;
You can verify assumptions before your assertions in your tests. For example, if the following is put at the beginning of a test, the rest of that test would only run on a system with “/” as the file separator (i.e., not Windows):
1   assumeTrue(File.separatorChar, is('/'));

When an assumption fails, the test is either marked as passing or ignored, depending on the version of JUnit.3

Spock

Spock is a testing framework for Java, Groovy, or any other JVM language. It takes full advantage of Groovy and has object mocking built-in. The Spock web site4 has this to say about Spock:
  • What makes it stand out from the crowd is its beautiful and highly expressive specification language. Thanks to its JUnit runner, Spock is compatible with most IDEs, build tools, and continuous integration servers. Spock is inspired from JUnit, RSpec, jMock, Mockito, Groovy, Scala, Vulcans, and other fascinating life forms.

Spock Basics

Test classes in Spock are called Specifications . The basic structure of a Specification in Spock is a class that extends spock.lang.Specification and has multiple test methods (which may have descriptive String names). The class’s name should end with Spec, for example, a Specification about a Vampire class could be named VampireSpec.

Spock processes the test code and allows you to use a Groovy-based syntax to specify tests. Spock tests should go under the src/test/groovy directory.

Each test is composed of labeled blocks of code with labels like when, then, and where. The best way to learn Spock is with examples.

To get started, first add Spock as a dependency to your project. For example, within a Gradle build file, put the following:
1   dependencies {
2    testImplementation "org.spockframework:spock-core:1.3-groovy-2.5"
3   }

A Simple Test

Let’s start by recreating a simple test:
1   def "toString yields the String representation"() {
2           def array = ['a', 'b', 'c'] as String[]
3           when:
4           def arrayWrapper = new ArrayWrapper<String>(array)
5           then:
6           arrayWrapper.toString() == '[a, b, c]'
7   }

As shown, assertions are simply Groovy conditional expressions. Every line after then: will be tested for Groovy truthiness. If the == expression returns false, the test will fail and Spock will give a detailed printout to explain why it failed.

In the absence of any when clause, you can use the expect clause instead of then; for example:
1   def "empty list size is zero"() {
2           expect: [].size() == 0
3   }

Mocking

Mocking is when you use a tool to extend an interface or class that can mimic the behavior of that class or interface within a test to assist testing your code. In JUnit tests, you need to use a library, like Mockito,5 to mock other classes that are not part of the test.

Mocking interfaces or classes is extremely easy in Spock.6 Simply use the Mock method, as shown in the following example (where Subscriber is an interface):
1   class  APublisher  extends  Specification {
2     def  publisher = new  Publisher()
3     def subscriber = Mock(Subscriber)
Now subscriber is a mocked object. You can implement methods simply using the overloaded >> operator as shown next. The following example throws an Exception whenever receive is called:
1   def "can cope with misbehaving subscribers"() {
2       subscriber.receive(_) >> { throw   new   Exception() }
3
4       when:
5       publisher.send("event")
6       publisher.send("event")
7
8       then:
9       2 * subscriber.receive("event")
10   }

Expected behavior can be described by using a number or range multiplied by (*) the method call, as shown here (it expects the receive method should be called two times).

The underscore (_) is treated like a wildcard (much like in Scala).

Lists or Tables of Data

Spock allows you to use lists or tables of data to more simply test multiple test cases within one test.

For example:
1   def "subscribers receive published events at least once"(){
2       when: publisher.send(event)
3       then: (1.._) * subscriber.receive(event)
4       where: event << ["started", "paused", "stopped"]
5   }

The overloaded << operator is used to provide a list for the event variable. Although it is a list here, anything that is Iterable could be used. This has the effect of running the test for each value in the list.

../images/435475_2_En_14_Chapter/435475_2_En_14_Figa_HTML.jpg Ranges

The range 1.._ here means “one or more” times. You can also use _..3, for example, to mean “three or fewer” times.

Tabular formatted data can be used as well. For example, the following test has a table with two columns, name and length:
1   def "length of NASA mission names"() {
2         expect:
3         name.size() == length
4
5         where:
6         name       | length
7         "Mercury"  | 7
8         "Gemini"   | 6
9         "Apollo"   | 6
10   }

In this case, the two columns (name and length) are used to substitute the corresponding variables in the expect block. Any number of columns can be used.

You can also add the @Unroll annotation so that each table row results in a separate test output. You can then use # to refer to a column. For example, change the previous test’s signature to the following:
1   @Unroll
2   def "length of NASA mission name, #name"() {

Expecting Exceptions

Use the thrown method in the then block to expect a thrown Exception.
1   def "peek on empty stack throws"() {
2       when: stack.peek()
3       then: thrown(EmptyStackException)
4   }
You can also capture the thrown Exception by simply assigning it to thrown(). For example:
1   def "peek on empty stack throws"() {
2       when: stack.peek()
3       then:
4       Exception e = thrown()
5       e.toString().contains("EmptyStackException")
6   }

After writing some tests, run the tests with either Gradle or Maven. For Gradle run ./gradlew test (results go under build/reports/tests/test/). For Maven run mvn test (results are under build/surefire-reports/).

Other Test Frameworks

There are many other test frameworks that unfortunately we do not have time to cover. Some are used to enable automated browser testing of web applications, like Geb7 and Selenium.8

Others enable BDD (behavior-driven development) like Cucumber.9 Cucumber enables tests to be written in close to plain English. For example, a scenario for testing some code from earlier in the book:
Scenario: Longest Dragon name
  Given a list of Dragons
  When the longest Dragon name is found
  Then the name is "Norbert"

Summary

In this chapter, you’ve learned how testing is a very important part of software development and the types of tests you should write. You’ve learned about how to use JUnit and Spock for running tests and learned that other test frameworks exist for running integration tests or BDD.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset