Software testing is a process used to verify that software:
According to the ANSI/IEEE 1059 Standard, Testing can be defined as:
"A process of analysing a software item to detect the differences between existing and required conditions (that is defects/errors/bugs) and to evaluate the features of the software item." In an organisation, a number of different individuals can be responsible for testing. Larger organisations typically have a team with responsibility for testing, but a number of different types of users could be involved:
There are a range of different types of testing including:
- Unit Testing - testing of small components (units) of code within developed software
- Functional Testing - testing of software functions as a "black box" by feeding them inputs and examining outputs
- Integration Testing - combining of modules (previously unit tested) and testing the modules as a combined group to examine expected outputs
- Usability Testing - testing how usable are developed systems and customer experiences with these systems. Do customers like the system?
- Compatibility Testing - testing how the software behaves under different operating systems, browser types and devices.
- many more...
While we could build an entire course/module on testing, we will focus on just one of these for this module -> Unit Testing
What is Unit Testing?Unit tests allow us to verify that small sections of software code are working correctly. "Units" are intended to be the smallest testable part of an application and generally would be code in a method or function. Unit tests are generally written and executed by developers to ensure that code meets design specifications and behaves as intended. There are a number of approaches that can be taken with Unit testing, but in essence it means that developers would code tests into every small unit of code they write.Why perform Unit Testing?From the description above, an obvious disadvantage is highlighted; developers are now spending more time writing tests in addition to the core code they are already expected to write. There is certainly a significant overhead with this on initial writing of code. So let us consider just a few of the advantages:
What is JUnit?JUnit is a unit testing framework for the Java programming language. It is open source and allows us to write and run repeatable tests and to incrementally build test suites to detect unintended side effects. Tests can be run continuously, with results provided immediately, meaning that problems can be detected and remedied quickly. The JUnit framework can be easily integrated with Eclipse, IntelliJ, Ant, Maven etc. and has it's own graphical user interface (GUI).
Installing JUnitInstalling JUnit is a relatively straightforward set of steps, considering that we have already set up environment variables previously for this module.
On Windows 7 open the search bar and type 'environment' and choose 'Edit the System Environment Variables' -> Environment Variables
CLASSPATH .;%JUNIT_HOME%\junit-4.11.jar;%JUNIT_HOME%\hamcrest-core-1.3.jar;
Assuming our following examples work correctly - JUnit has now been set up correctly.
Running Some TestsSo let's write our first test class. This class has one test for now, which does a simple string compare assertion (more on this later!).
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class MyTest {
@Test
public void firstTest() {
System.out.println("MyTest: Inside firstTest()");
String str= "First Test Passed";
assertEquals("First Test Passed",str);
}
}
Source file: MyTest.java
To compile this code, navigate to the folder where you have saved the file and use:
javac MyTest.java If this completes successfully, you will not see any output. A new file will have been created in the same folder 'MyTest.class'
You may have noticed that this Java class does not have a main method (nor is it an applet/servlet/JSP etc.) so it can not be run directly using 'java MyTest'. If you attempt to do this, you will receive a 'Main method not found' error message.
So let us create a class which will run our test(s).
import org.junit.runner.*; import org.junit.runner.notification.Failure; public class MyTestRunner { public static void main(String[] args) { Result result = JUnitCore.runClasses(MyTest.class); for (Failure failure : result.getFailures()) { System.out.println(failure.toString()); } System.out.println("Tests Successful = " + result.wasSuccessful()); } } Source file: MyTestRunner.java (TODO)
After compiling with javac, we can now perform the following:
java MyTestRunner to receive the following output:
Tests Successful = true Congratulations - you have just written your first test!
Example #1Consider the following code. The purpose of this code is to take a single argument and test whether this argument is a prime number. The definition of a prime number is :
"An integer greater than one is called a prime number if its only positive divisors (factors) are one and itself. " import java.math.BigInteger; class CheckPrime { public static void main(String[] args) { System.out.println("Welcome to TestPrime"); if (args.length!=1) { System.out.println("Please provide a number, in the format: java CheckPrime 15"); System.exit(0); } else { System.out.println("Your inputted number is " + args[0]); System.out.println("Prime: " + isPrimeMethod(args[0])); } } public static boolean isPrimeMethod(String s) { BigInteger i = new BigInteger(s); return i.isProbablePrime(1); } } Source: CheckPrime.java
Question:i) Describe a scenario where the method "isPrimeMethod(String s)" will fail.
ii) Create a unit test class 'PrimeTest.java' which will test this scenario and report a failure.
iii) Fix the code in CheckPrime.java to satisfy the test from part ii)
Notes: The following PrimeTestRunner.java file has been provided.
You can assume that the certainty value of "isProbablePrime(certainty)" method will be accurate. In reality this is quite a complex method, which allows a trade-off of speed vs accuracy using various mathematical algorithms.
import org.junit.runner.*; import org.junit.runner.notification.Failure; public class PrimeTestRunner { public static void main(String[] args) { Result result = JUnitCore.runClasses(PrimeTest.class); for (Failure failure : result.getFailures()) { System.out.println("Test Failure: " + failure.toString()); } System.out.println("Tests Successful = " + result.wasSuccessful()); } } Source: PrimeTestRunner.java
Solution:i) The isPrimeMethod(String s) method will fail in the case where 's' cannot be converted to a BigInteger. For example, we will not be able to run the application with 'java CheckPrime abc'. This will cause a NumberFormatException at run time.
ii) The following file contains two tests, one which checks normal numeric operation and one which tests non-numeric operation. If a non-numeric parameter is passed in, we should expect isPrimeMethod() to return false. This test will report a failure in this case though due to a NumberFormatException.
import org.junit.Test; import static org.junit.Assert.assertEquals; public class PrimeTest { @Test public void testPrimeNumeric() { System.out.println("PrimeTest: testPrimeNumeric()"); assertEquals(true,CheckPrime.isPrimeMethod("5")); } @Test public void testPrimeNonNumeric() { System.out.println("PrimeTest: testPrimeNonNumeric()"); assertEquals(false,CheckPrime.isPrimeMethod("a")); } } Source: PrimeTest.java
iii)
To fix our code, we should handle the NumberFormatException that would be thrown in this case. We can do this easily by catching the exception and returning false.
import java.math.BigInteger;
class CheckPrime { public static void main(String[] args) { System.out.println("Welcome to TestPrime"); if (args.length!=1) { System.out.println("Please provide a number, in the format: java CheckPrime 15"); System.exit(0); } else { System.out.println("Your inputted number is " + args[0]); System.out.println("Prime: " + isPrimeMethod(args[0])); } } public static boolean isPrimeMethod(String s) { try { BigInteger i = new BigInteger(s); return i.isProbablePrime(1); } catch (NumberFormatException e) { return false; } } } Source: CheckPrime.java
When to test? (How small are units?)When we write unit tests, we are testing our code and not the compiler. A good example of this relates to asking 'Should we test get() and set() methods in our JavaBeans?'. Let us consider a simple Java bean similar to those encountered earlier in the course:
public class User {
private String firstname; private String surname; public User(String firstname, String surname ) { this.firstname = firstname; this.surname = surname; } public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } } Source: User.java
Now let us create a test like the following:
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class UserTest {
@Test
public void testGetSetFirstname() {
System.out.println("UserTest: Inside testGetSetFirstname()");
User user = new User();
user.setFirstname("David");
assertEquals("David", user.getFirstname());
}
}
Source: UserTest.java
The previous 'MyTestRunner' can be modified slightly as below, executed using 'java MyTestRunner' and all of our tests should be performed and passed.
Result result = JUnitCore.runClasses(MyTest.class,UserTest.class); But what have we actually tested?
Let us consider our method in a shorter form:
public void testGetSetFirstname() { firstname = "David"; assertEquals("David", firstname); } and let us go a step further:
public void testGetSetFirstname() { assertEquals("David", "David"); } By the time we have written our test in this minimised format, we will have realised that what we are testing is the Java compiler and not actually any particular functionality we have written ourselves. As a developer, it is not your responsibility to test Java (let Oracle do this!). In fact, your starting point as a Java programmer needs to be to assume that the programming language will perform as expected.
It is for this reason that get() and set() methods would not generally be tested. However, it might be appropriate where the developer has written a more complex get() or set() method which has some form of implemented business logic such as parsing, sub-stringing, null-checking or appending. Unit tests are designed to identify scenarios where something might break. If you have written code that might break, then it is appropriate to add in a unit test.
Equally, you might want to test whether an object has already been set appropriately, which can be done by adding a test method to test the constructor. For example:
@Test public void testCreateUser() { assertEquals("David", new User("David", "Molloy").getFirstname()); } This test makes sure that we have initialised our variables correctly in the constructor and can be useful where we might have different constructors initialising things differently.
Taking things a step furtherTaking our User class (we'll now call it Client to avoid confusion) and the constructor idea as a starting point, let us create a new scenario. We want to add in a 'getInitials()' method which will rely on some business logic to have been previously performed in the constructor. Let's look at a full class here that we wish to test:
public class Client { private String firstname; private String surname; private String initials; public Client() {} public Client(String firstname, String surname) { this.firstname = firstname; this.surname = surname; this.initials = firstname.substring(0,1) + surname.substring(0,1); } public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public void setInitials(String initials) { this.initials = initials; } public String getInitials() { return initials; } } Source: Client.java
The part that we are really interested in testing is whether the code marked in red will function correctly for all test cases. A more experienced programmer is going to immediately recognise certain scenarios where this code would either success or fail. Examples:
These are known as runtime exceptions, meaning that the problems only occur at run time and not at compilation time (we had no problem using 'javac'). The problems will occur in some scenarios and not in others - hence the usefulness for testing.
We have three main options here:
1) Write the production code, do some local ad-hoc testing and consider the code working (Not a formal testing approach)
2) Write the production code, then write some formal unit tests and ensure they pass (As we did in Example 1)
3) "Test Driven Development"
Test-Driven DevelopmentUpon identifying the business logic to be implemented, the majority of software developers tend to jump straight into writing code. The code is written, loosely tested for functionality and then deployed.
Test-driven development is a fundamentally different approach focused around test-first development where the software developer writes tests before writing any production code. This encourages the developer to think through the design and requirements of the software system before writing functional code.
1) The developer firstly writes an automated test case which defines the new functionality or business logic to be implemented.
2) This should initially fail as the production code should not yet exist. If the test does not fail, it is either dysfunctional or the functionality already exists in the production code.
3) After the test has been written, the developer writes the minimal amount of production code required to pass the test. At this stage, the code is not expected to be elegant or final in any way, but should be enough to pass the test.
4) All of the tests are now run again - If this test succeeds, then the code meets all of the tested requirements.
5) Code can now be cleaned up, duplication removed, comments added. Following this stage, we should have final, clean production code which covers this new functionality and any functionality previously implemented under our test-driven process.
Test-driven Development Cycle - Source: Wikipedia : (http://en.wikipedia.org/wiki/File:Test-driven_development.PNG)
Let's start again, using test-driven development. Our starting code will look like this:
public class Client{ private String firstname; private String surname; public Client(String firstname, String surname) { this.firstname = firstname; this.surname = surname; } public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; }
} Source: Client.java
Let us now consider that we have been given some form of design brief:
1) We need to provide a method whereby we can get the initials of a client (first letter of firstname followed by first letter of surname). E.g. "David Molloy -> "DM"
2) If either surname or firstname are null they should be replaced by a blank character for the purposes of initials
3) If either surname or firstname are blank they should be replaced by a blank character for the purpose of initials
Because we are using TDD, we are not going to write production code yet. Instead, we will write a test first! In fact, we are going to write three separate tests to cater for the three points of functionality in the design brief. The first of these is to test the standard scenario (non-null and non-blank)
import org.junit.Test; import static org.junit.Assert.assertEquals; public class ClientTest { @Test public void testInitialsStandard() { System.out.println("ClientTest: Inside testIntialsStandard()"); Client client = new Client("David", "Molloy"); assertEquals("DM",client.getInitials()); } } We will need to write a simple runner class in order to run this test (and subsequent tests).
import org.junit.runner.*; import org.junit.runner.notification.Failure; public class ClientTestRunner { public static void main(String[] args) { Result result = JUnitCore.runClasses(ClientTest.class); for (Failure failure : result.getFailures()) { System.out.println("Test Failure: " + failure.toString()); } System.out.println("Tests Successful: " + result.wasSuccessful()); } } Source: ClientTestRunner.java
After compiling, and running our ClientTestRunner we FAIL the test as expected (and hoped):
As this test, has now failed, the next step is to write some production code to make it pass. We can do this easily and have in fact done this before.
We simply edit Client.java to add in this new functionality.
public class Client{ private String firstname; private String surname; private String initials; public Client(String firstname, String surname) { this.firstname = firstname; this.surname = surname; this.initials = firstname.substring(0,1) + surname.substring(0,1); } public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public String getInitials() { return initials; } public void setInitials(String initials) { this.initials=initials;} } Source: Client.java
Now, we can run our test again and we will see that the test passes!
There is nothing much we can do to "clean up" the code with such a simple example. This would be done at this point if appropriate.
Let's add a test for the second piece of functionality (Number 2 above) - we want to test for the cases where either the firstname is null or the surname is null. We don't try to cater for this in the production code (yet!), but instead we write the test.
import org.junit.Test; import static org.junit.Assert.assertEquals; public class ClientTest { @Test public void testInitialsStandard() { System.out.println("ClientTest: Inside testIntialsStandard()"); Client client = new Client("David", "Molloy"); assertEquals("DM",client.getInitials()); }
@Test public void testGetInitialsWhereNull() { System.out.println("ClientTest: Inside testGetInitialsWhereNull()"); Client client = new Client(null, null); assertEquals("",client.getInitials()); client= new Client("David", null); assertEquals("D",client.getInitials()); client= new Client(null, "Molloy"); assertEquals("M",client.getInitials()); }
} Source: ClientTest.java
Similar to before, if we run our ClientTestRunner we expect this test to FAIL. The production code has not yet been written to cater for these scenarios. The output should look as follows:
In line with the diagram, the next step is to write some production code to allow this test to pass. You can see that the code has changed fairly fundamentally to cater for these new null scenarios.
public class Client{ private String firstname; private String surname; private String initials; public Client(String firstname, String surname) { this.firstname = firstname; this.surname = surname; initials = ""; if (firstname!=null) initials = initials + firstname.substring(0,1); if (surname!=null) initials = initials + surname.substring(0,1); } public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public String getInitials() { return initials; } public void setInitials(String initials) { this.initials=initials;} } Source: Client.java
Again, running the ClientTestRunner, we get:
So far so good. Normally, at this point we would "clean up" the code, but there is not a lot of improvement that could be made here.
So on to the final test, the handling of blank values for either the firstname or surname. We create the test first:
import org.junit.Test; import static org.junit.Assert.assertEquals; public class ClientTest { @Test public void testInitialsStandard() { System.out.println("ClientTest: Inside testIntialsStandard()"); Client client = new Client("David", "Molloy"); assertEquals("DM",client.getInitials()); } @Test public void testGetInitialsWhereNull() { System.out.println("ClientTest: Inside testGetInitialsWhereNull()"); Client client = new Client(null, null); assertEquals("",client.getInitials()); client= new Client("David", null); assertEquals("D",client.getInitials()); client= new Client(null, "Molloy"); assertEquals("M",client.getInitials()); } @Test public void testGetInitialsWhereBlank() { System.out.println("ClientTest: Inside testGetInitialsWhereBlank()"); Client client = new Client("", ""); assertEquals("",client.getInitials()); client = new Client("David", ""); assertEquals("D",client.getInitials()); client = new Client("", "Molloy"); assertEquals("M",client.getInitials()); } } Source: ClientTest.java
Running ClientTestRunner we can see that this test FAILS as expected:
Let us put in our final bit of production code to satisfy this test:
public class Client{ private String firstname; private String surname; private String initials; public Client(String firstname, String surname) { this.firstname = firstname; this.surname = surname; initials = ""; if ((firstname!=null)&&(firstname.length()>0)) initials = initials + firstname.substring(0,1); if ((surname!=null)&&(surname.length()>0)) initials = initials + surname.substring(0,1); } public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } public String getInitials() { return initials; } public void setInitials(String initials) { this.initials=initials;} } Source: Client.java
However, what is important is that we now have written our first test-driven development code. We wrote the test code to rigorously test the functionality requirements and used these tests to help write and test our production code.
Is this process not considerably slower? Test-driven development is almost certainly slower in any single developer systems. However, it does provide more robust and stable code. It seems somewhat slower than it is in this example, but as the developer gets familiar with writing tests, it becomes a much quicker process. Initial development tends to be slower, but the benefits are returned at later stages, particularly in multi-developer systems.
|
Course Content >