Week 15- Test Driven Development_UnitTesting
Week 15- Test Driven Development_UnitTesting
UNIT TESTING
What is Test Driven Development?
The requirements are defined, from these requirements, the development team creates the design of
a specific part of the application, and the developers then write the necessary production code. They
will then document the code and create tests (manual and/or automated) to test that the system
works as expected. The testers then run the tests, and any bugs found are then fixed.
This approach means that (a) you are only testing the system based on the code that was already
written, and (b) when you change the system to fix one of the identified bugs, there is not an
automated way to make sure you have not changed the design or introduced new bugs.
With a test-driven-development approach, the process is
somewhat different:
The requirements are used to directly create the acceptance tests (the ones that
determine if the system meets the needs of the users / stakeholders as defined by
the requirements).
The team uses the requirements and acceptance tests to design the system and create
the appropriate unit tests. These tests are now executed, and (since nothing yet
exists) will naturally fail. However the unit tests are now the detailed definition of the
requirements.
The team then writes the production code so that the tests now pass. The
new code written at this stage is not perfect, and may, for example, pass
the test in an inelegant way. That is acceptable because later steps
improve it. At this point, the only purpose of the written code is to pass
the test.
Now the code should be cleaned up as necessary. Move code from where it
was convenient for passing the test to where it logically belongs. Remove
any duplication you can find. Make sure that variable and method names
represent their current use. By re-running the test cases, the developer
can be confident that code refactoring is not damaging any existing
functionality.
What is Acceptance TDD and Developer TDD?
There are two levels of TDD:
Acceptance TDD (ATDD): With ATDD you write a single acceptance test.
This test fulfills the requirement of the specification or satisfies the
behavior of the system. After that write just enough
production/functionality code to fulfill that acceptance test. Acceptance
test focuses on the overall behavior of the system. ATDD is also known
as Behavioral Driven Development (BDD).
Developer TDD: With Developer TDD you write single developer
test i.e. unit test and then just enough production code to fulfill
that test. The unit test focuses on every small functionality of the
system. Developer TDD is simply called as TDD. The main goal of
ATDD and TDD is to specify detailed, executable requirements for
your solution on a just in time (JIT) basis. JIT means taking only
those requirements in consideration that are needed in the system.
So increase efficiency.
Advantages of TDD:
1. Better Designed, cleaner and more extensible code.
It helps to understand how the code will be used and how it interacts
with other modules.
It results in better design decision and more maintainable code.
TDD allows writing smaller code having single responsibility rather
than monolithic procedures with multiple responsibilities. This makes
the code simpler to understand.
TDD also forces to write only production code to pass tests based on user
requirements.
2. Confidence to Refactor.
If you refactor code, there can be possibilities of breaks in the code. So
having a set of tests you can fix those breaks before release.
3. Good for teamwork.
In the absence of any team member, other team members can easily pick
up and work on the code. It also aids knowledge sharing, thereby making
the team more effective overall.
4. Good for Developers.
Though developers have to spend more time in writing TDD test cases, it
takes a lot less time for debugging and developing new features. You will
write cleaner, less complicated code.
What is Unit Testing?
Here in this example, we will define a class password. For this class, we
will try to satisfy following conditions.
A condition for Password acceptance:
The password should be between 5 to 10 characters.
First, we write the code that fulfills all the above requirements.
Scenario 1: Torun the unit test, we create class PasswordValidator.
Now, We will run above class TestPassword. Output is PASSED as shown
below:
Scenario 2: Here we can see in method TestPasswordLength() there is no
need of creating an instance of class PasswordValidator. Instance means
creating an object of class to refer the members (variables/methods) of
that class.
We can call the isValid () method directly by
PasswordValidator. IsValid ("Abc123"). (See image below)
So we Refactor (change code) as below:
Scenario 3: After refactoring, the output shows failed status (see image
below) this is because we have removed the instance. So there is no
reference to non–static method isValid ().
So we need to change this method by adding "static" word
before Boolean as
public static boolean isValid (String password)
Refactoring Class PasswordValidator () to remove above error
to pass the test.
Output:
After making changes to class PasswordValidator() if we run
the test then the output will be PASSED as shown below.
Assert Methods:
In JUnit, multiple assert methods can be used to test various conditions within a single
test method.
1. assertTrue and assertFalse: These methods verify whether a condition is true or
false, respectively:
import static org.junit.Assert.*; import static org.junit.Assert.*;
import org.junit.Test; import org.junit.Test;
@Test @Test
public void testBooleanConditions() { public void testBooleanConditions() {
assertTrue(5 > 2); // Passes because 5 is greater than 2 assertFalse(5 > 2); // Fails because 5 is greater than 2
assertFalse(3 < 1); // Passes because 3 is not less than 1 assertTrue(3 < 1); // Fails because 3 is not less than 1
} }
} }
2. assertNotNull and assertNull
@Test @Test
public void testNullConditions() { public void testNullConditions() {
Object obj = new Object(); Object obj = null;
assertNotNull(obj); // Passes because obj is not null assertNotNull(obj); // Fails because obj is null
@Test @Test
public void testArrayEquality() { public void testArrayEquality() {
int[] expectedArray = {1, 2, 3}; int[] expectedArray = {1, 2, 3};
int[] resultArray = {1, 2, 3}; int[] resultArray = {1, 3, 2}; // Different order
Test Class: Create a Java class for the test case. This class will contain
one or more test methods.
public class MyTestCase {
// Test methods will be defined here
}
Test Methods: Write methods to test specific units (usually methods) of
your code. Each test method should be annotated with @Test to indicate
that it's a test method.
@Test
public void testAddition() {
// Test logic for addition
assertEquals(5, Calculator.add(2, 3)); // Example assertion
}
Assertions: Use assertion methods from the Assertions class to validate the expected
behavior of the code being tested. Assertions verify whether the actual result matches the
expected result.
@Test
public void testAddition() {
assertEquals(5, Calculator.add(2, 3)); // Example test case
}
}
JUnit Annotations:
@AfterEach
public void tearDown() {
// Clean up after the test
}
JUnit Annotations:
@Disabled:
Temporarily disables a test method without removing it from the test
suite. Useful when a test case is not yet implemented or needs to be
skipped temporarily.
@Timeout:
Specifies a maximum execution time for a test method. If the test
method takes longer than the specified time, it will fail.
@Test
@Timeout(5) // Time in seconds
public void testMethodWithTimeout() {
// Test logic that should execute within 5
seconds
}
Parameterized testing in JUnit
Parameterized testing in JUnit allows you to run the same test logic with
different sets of parameters. It's useful when you want to test a method
or functionality with multiple inputs and ensure that it behaves correctly
across various scenarios. JUnit provides @ParameterizedTest to enable
parameterized testing.
Let's say we want to test a simple Calculator class's
addition method using parameterized testing.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
@ParameterizedTest
@CsvSource({ "2, 3, 5", "-1, 1, 0", "10, -5, 5" }) // Parameters: input1, input2, expectedSum
public void testAddition(int a, int b, int expectedResult) {
Calculator calculator = new Calculator();
int result = calculator.add(a, b);
assertEquals(expectedResult, result);
}
}
In this example:
@Test
public void testAddition() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result); // Failing test
}
}
Write Minimum Code to Pass Test (Green):
public class Calculator {
public int add(int a, int b) {
return a + b; // Minimum code to pass the test
}
}
TDD with JUnit encourages developers to focus on writing testable and reliable code, resulting in better-designed
software and a comprehensive suite of tests to validate its behavior.
Lets conclude…
“Unit testing” is writing many small tests that each
test one very simple function or object behavior. TDD is
a thinking process that results in unit tests, and
“thinking in tests” tends to result in more fine-grained
and comprehensive testing, and an easier-to-extend
software design.