Readers of the Software Testing Topics
Developers and Application Architects.
What will you learn from the topics?
Most of the time, we know the difference between Unit Testing and Integration Testing but when we start doing Unit testing, we mix Unit Test with an Integration Testing. Thus, I will explain the principles and best practice about Unit testing. We will know step by step how to write the test cases, using Behavior Driven Development (BDD) during White-box testing. The following concepts will be covered
- Best Practices and Principles.
- Writing Test Scenario, Test Case and Test Data.
- Naming Convention of Test Method, using Behavior Driven Development (BDD).
- How to mock an object.
- Unit Testing compatible Class Design.
- Tight coupling is the obstacle for Unit testing.
- Loose coupling for Unit testing.
- Dependency type.
- Stub and Driver for an Integration Testing
- Integration Testing Approach
Benefits of Software Testing
- Reduce risk of failures when the systems are transferred to production or live operation.
- Documental proof that business requirements have been met.
- Assurance that the user is able to operate designed solution productively.
- Assurance that the system works properly with the existing legacy systems.
Software Tester - Developer vs. Quality Assurance Engineer
Testing as a Developer
Developer can verify their implemented logics or codes according to the business requirement. Their main goal is to make sure that all the logics can run smoothly for position data. They also have to make sure that if end user passes any unexpected data, then the logics can still handle the exception for the negative or exceptional data and show the proper messages for it. This kind of testing is known as White-Box Testing because they can see their internal logics or codes. For example, an automated Unit-testing or an Automated Integration-testing.
Testing as a Quality Assurance (QA) Engineer
QA can verify the software according to business requirement but they don't need to worry about the logics or codes. They are just like an end user. They will input the positive data and they will expect the positive functionality from the output data. On the other hand, they will input the wrong data to make sure that they are getting the proper message for the wrong input. This kind of testing is known as a Black-Box Testing. Due to this, they can't see the internal logics or codes. They are verifying the external behavior from this test. For example, Acceptance testing.
Let's drilldown the basic testing concept
Test Document Basic
- Test Case – A set of conditions and steps to determine whether a system satisfies the requirements correctly or not. This is described by test data, environment and the expected result.
- Test Suite – It is a collection of test cases.
- Test Plan – It is a document describing testing approach, test suites and test cases.
- Test Strategy – It is a way to identify test cases from a specification or an implementation.
- Test Effectiveness – It is the relative ability of test strategy to find the bugs.
- Test Efficiency – It is the relative cost of finding a bug.
- Test Coverage – It is the percentage of testable elements, which have been tested.
Test case & Test scenario
If you have to withdraw money from an ATM machine, then it is a scenario but to withdraw money, you need to execute many test cases.
Test-Case
A Test Case is a condition, which is executed for the expected output with the predefined set of steps with the known inputs.
Test scenarios
Test scenarios have multiple Test Cases. Thus, while starting testing, first prepare Test scenarios, followed by creating Test Cases for each scenario.
Example for Test Case & Test Scenario
Test Scenario-101
Checking the functionality of Login button.
Test Cases for this Test Scenario-101,
- Test-Case1 - Click the button without entering the user-name and password.
- Test-Case2 - Click the button only after entering the user-name.
- Test-Case3 - Click the button, while entering the wrong user-name and wrong password etc.
These all test-cases will have some expected, unexpected and an actual result.
Term of mistake
- Error - It is a mistake made by the developers during an implementation of a software system.
- Failure - It is an incorrect behavior of a program.
- Fault - It is an incorrect code, which caused a failure.
- Incident - The symptoms associated with a failure.
- Bug - It is an error or a fault.
- Fault Directed testing – It helps in finding the bugs through the failure.
Positive Testing & Negative Testing
Positive Testing - - When the tester tests the Application with the valid input/data from positive point of view, then it is known as Positive testing.
Negative Testing - - When the tester tests the Application with an invalid input/data from negative point of view, then it is known as Negative testing.
White-Box & Black-Box Testing
Depending on the developers and QA Team, we can say that we have 2 types of testing. The developers can view the internal logics of the Implemented Application system or development box. This box is visible to the developer and it's called White-Box. On the other hand, QA team can't see the logical implementation and they can see the functionality of the system as an end user point of view. Due to this, the box is black and they test the external behaviors. It is called Black-Box testing.
Type of Tests depending on developer & QA Team
- White-Box Testing
- Black-Box Testing
White Box Testing
Advantages of White Box testing
- Reveals the bugs in the hidden code.
- Forces reason in an implementation for positive, negative and exceptional cases.
- Get confident and documented proof.
- Improves the design and the code quality.
Disadvantages of White Box testing
- Expensive compared to time and design.
- Need proper knowledge about testing methodology.
- Cases missed can miss our code.
Black Box testing
Advantages of Black Box testing
- Tester and programmer are independent.
- Testing from user’s point of view.
- Test cases can be prepared right after the specification is decided.
Disadvantages of Black Box testing
- Duplication of test cases by the testers and the programmer
- Only simple testing can be done as all the cases can’t be build and tested
Black Box vs. White Box testing approach
Black-Box approach
- Field level check.
- Field level validation.
- User interface check.
- Function level check.
White Box approach
- Statement coverage.
- Decision coverage.
- Condition coverage.
- Path coverage.
Type of testing
- Unit Testing.
- Integration Testing.
- Functional Testing.
- System Testing.
- Stress Testing.
- Performance Testing.
- Usability Testing.
- Acceptance Testing.
- Regression Testing.
- Beta Testing.
Most common testing in many organizations
Granularity Levels
- Unit Test - Verification of the single classes.
- Module Test - Testing interaction of the groups of the classes (package).
- Integration Test - Verification of the interactions between the modules/components.
- Functional Test - Verification of the external behaviors of the modules, components and system.
- System Test - Testing of the system against the objectives.
- Acceptance Test - Validation of application against the user requirements.
- Regression Test - Re-running all the tests on system when it is changed.
Unit testing
In this section, I will discuss in more detail about Unit testing. You will learn more about the testing principles and best practices. At the same time, I will show you complete practical examples step by step.
High level concept about Unit testing
This is White-box testing; where the developer checks their internal logics and functionality. Some developers don't like Unit testing. Another problem is sometimes they mix Unit testing with an Integration testing.
Top five excuses for not doing Unit testing
- I don’t have time to do Unit test.
- The office pays me to write down the codes, not to write down Unit test.
- I am supporting a legacy Application without Unit tests and existing design is not suitable for Unit test.
- QA and User Acceptance Testing are far more effective in finding the bugs.
- I don’t know how to write Unit test.
Advantages of Unit test
- Reduces the level of bugs in production code.
- Saves your development time.
- Automated tests can be run as frequently, as required.
- Make easier to change and refactor the code.
- Improve the design of the code especially with Test-Driven-Development.
- A form of documentation.
- Inspires confidence.
- Measures of completion.
Disadvantages of Unit test
- Not implementable to logical operators.
- Insensitive to the loop operators (number of iterations).
Best practices and principles to write Unit testing
Principles to write Unit testing
Principle 1. “Test the logic of the class only, nothing else”
Note that this is one of the most important principles during Unit testing. When you are going to test a class, you should not have dependency on the database, file, registry, Web Services etc. You should be able to test any class in complete isolation and it should be designed to support complete isolation.
According to this principle, mock out all the external Services and state and remember Unit test never uses
- Configuration settings.
- A database.
- Another Application/Service/file/network I/O or file system.
- Logging.
Principle 2. “Fail first and set a guard on the door"
Before you write piece of logic, first write your fail test. Afterwards, add or refactor your guard logics and throw proper exceptions or messages for the negative or an exceptional data. Finally, run it and pass the test.
Explanation of Unit Testing principles
Principle 1 - Suppose, you have a method 'IsValidUser' to verify the user name and password for the login.
Now, if you look at the line number 15 and 17, then you will see class 'UserLogIn' has a dependency to the class 'UserDataAccess' and it is tightly coupled. Now, the problem is you need to test a method 'IsValidUser' of a class class 'UserLogIn' and you have to avoid the dependency. If you call 'sValidUser', then it will call the method GetUserInfoByUseeName(userName) and according to Unit test principle, if we have a dependency to a method of another class, then we have to mock that object class. It means that we have to inject some dummy data to that object. In this example, we need to inject dummy data for the 'GetUserInfoByUseeName(userName). If we do so, then we will be able to verify the logics of the method ('IsValidUser()') in the 'UserLogIn' class.
In order to avoid the tight coupling code, we need to refactor the code. Later, I will explain what are dependency, tight coupling and loose coupling. Now, my main goal is to make you know Unit testing principle only. Let's refactor the class again.
Now, the class 'RefactoringUserLogIn' is loosely coupled and you can inject the implemented class of the 'IUserDataAccess' ,which will make your life easy to do Unit testing. Note that there are many ways to inject the implemented class. Later, I will show you that as well as how to mock an object of a class. During Unit testing, if you mock the 'IUserDataAccess', it means that you are implementing a dummy object of 'IUserDataAccess' and the method 'IsValidUser()' , it will be not able to get any data directly from the database or ORM via 'UserDataAccess' class.
Principle 2
More than 50 percent people fail their road driving test first time, then after practicing on the highway and seeking tips from others, they are ensuring their success in the road test. If you are already familiar with the Test-Driven-Development (TDD), then you are already familiar with the fail test. I will explain TDD at the end of this section.
Let me explain the guard logics in the method and how it will help you to pass your Unit testing. Let's say that we have a method ‘GetSumByPositiveNumber()’ and it has two parameters. The business requirements are always to pass the positive numbers and get the sum. Never pass the negative or zero values for the methods.
Now, according to your business requirement, if you pass the values of the parameters like 1 and 2, then the method will return 3, which is correct but what happens, if you pass the negative values as a parameter or some combination like (-5, -1), (-5, 1), (5, 0), (0, 0) etc.
If we put some guard logics into the method, then the test should be passed. We are throwing the exception for the negative data.
Best practices for writing Unit Testing
- If the old test fails for the extension of the method, which is introduced, then remove or enhance old test and avoid requirement conflict.
- If you remove a single line in your tested class and all the tests still pass, then you do not have enough unit tests.
- Developer will often run the test, so make the test easy to run.
- Test only public methods and public interface of the component
- Don't create instances of the classes directly inside a Unit test - use Factory Method.
- Refactor the unit test and keep it simple. Remember that the tests must be maintained by any code - so it's better to keep it simple.
- Any test should run in any order; so avoid dependencies between the tests.
- Test should be readable like a book; so write the comments in Asserts. Write the descriptive method names. Use Behavior-Driven-Development (BBD) technique.
- Test everything that can possibly break.
- Test everything, but not Private methods
Writing test scenario, test case and test data
Only Three Test Cases for Unit Testing, Nothing Else
- Positive Test Cases - Correct data to check for the correct output.
- Negative Test Cases - Broken or missing data to check for the proper handling.
- Exception Test Cases - Giving an unexpected data or behavior and check for the exception caught properly or not.
Test data according to the test cases
Let’s set some test data and consider “GetSum()” as an example. Here, my main goal is to show you how can we set some test data for the test cases.
Type of test data
- Positive Data
- Negative Data
- Exceptional Data
Positive data for positive test cases
The main goal of the positive test cases is to verify the functionality of the logics.
TestCase-1 - Given positive values should return the expected result
- TestData-1 - Set Input Parameters as firstNumber =1, secondNumber=1
Negative Data for Negative Test Cases
The main goal of the negative test cases is to get proper messages for the bad input according to your business requirements.
TestCase-2 - Given invalid values, you should produce invalid argument message.
- TestData-2 - Set the input parameters as firstNumber =-1, secondNumber =-1
- TestData-3 - Set the input parameters as firstNumber =-1, secondNumber = 1
- TestData-4 - Set the input parameters as firstNumber = 0, secondNumber = 1
- TestData-5 - Set the input parameters as firstNumber = 0, secondNumber =-1
- TestData-6 - Set the input parameters as firstNumber = 0, secondNumber = 0 etc.….
3.3.2.1.3 Exceptional data for exception test cases
The main goal of the exceptional test cases is to find out the proper exception handling with the proper messages, so that your code can’t break for the threshold limits.
Here, you can set the threshold limits of your test data. In .NET, the minimum value for a variable of type int is -2147483648 and the maximum value for a variable of type int is 2147483647.
TestCase3 - Given threshold limit values should throw an exception message.
- TestData-7 - Set the input parameters as firstNumber =1, secondNumber =2147483649
- TestData-8 - Set the input parameters as firstNumber =1, secondNumber = -2147483649
- TestData-9 - Set the input parameters as firstNumber =2147483649 secondNumber = -2147483649
- TestData-10 - Set the input parameters as firstNumber =2147483647, secondNumber=2147483647 etc.….
Naming convention of Test Method for Unit testing
Traditional principle of Unit Test
One Test Method is written to test one and only one method and one assert method should test only one expectation at a time.
In a short the principle says – “one function/method and one assert per test method”
So, let’s consider the bellow example
Comparing the Traditional Principle to the Real World
Test Scenario
Verify the “GetSum” Method
Test Cases
Positive Test Cases
TC1 - Given positive values, should return expected result
Test Data-1 - firstValue =5, secondValue =6
Negative Test Cases
TC2 - Given zero values, should produce invalid argument message
Test Data-2 - firstValue =0, secondValue =0
TC3 - Given negative values, should produce invalid argument message
Test Data-3 - firstValue =-5, secondValue =-6
Exceptional Test Cases
TC4 - Given threshold limit values, should throw exception message
Test Data-4 - firstValue =2147483647, secondValue =2147483647
Test Method Example
Now according to the traditional principle, let’s write the test method for the “GetSum”
Now according to the traditional principle we have covered the Positive Test Case with “Test Data-1”. But what about negative and exceptional test cases??
How do we cover the negative and exceptional test cases with traditional principle??
Behavior Driven Development (BDD)
Why Do We Need BDD
If we want to cover all of the behaviors of our test cases according to our previous example then we need to follow some technique so that we can write down all of the behaviors of the method. So, The BDD is the technique which gives us the opportunity to fulfillment for all of the test cases with standard and readable naming convention. Many peoples, many minds. There are many techniques to write the naming convention of the test method. But if really depends on you and your preference. There is nothing right or wrong if you follow some other technique. Anyway, in a short we can say that In BDD, components test their expected behavior.
Concept of BDD
- Given I am a beginner to the BDD technique,I never used this technique before
- When I read this tutorial for BDD,
- then I have started liking it and I learn it.
BDD Naming convention
Test Scenario
Verify the “GetSum” Method
Test Cases
Positive Test Cases
TC1 - Given positive values, should return expected result
Test Data-1 - firstValue =5, secondValue =6
Test Method - Naming Convention,
GivenPositiveVaidValuesAsParams_WhenGetSumIsCalled_ThenItShouldReturnSumValue
Given_Positive_Vaid_Values_As_Params_When_GetSum_Is_Called_Then_It_Should_Return_Sum_Value
Negative Test Cases
TC2 - Given zero values, should produce invalid argument message
Test Data-2 - firstValue =0, secondValue =0
Test Method - Naming Convention,
GivenZeroValuesAsParams_WhenGetSumIsCalled_ThenItShouldThrowInvalidArgumentException
Given_Zero_Values_As_Params_When_GetSum_IsCalled_Then_It_Should_Throow_Invalid_Argument_Exception
TC3 - Given negative values, should produce invalid argument message
Test Data-3 - firstValue =-5, secondValue =-6
Test Method - Naming Convention,
GivenNegativeValues_WhenGetSumIsCalled_ThenItShouldThrowInvalidArgumentException
Given_Negative_Values_When_GetSum_Is_Called_Then_It_Should_Throw_Invalid_Argument_Exception
Exceptional Test Cases
TC4 - Given threshold limit values, should throw exception message
Test Data-4 - firstValue =2147483647, secondValue =2147483647
Test Method - Naming Convention
GivenMaxLimitValuesOfIntAsParams_WhenGetSumIsCalled_ThenItShouldThrowSumException
Given_Max_Limit_Values_Of_Int_As_Params_When_GetSum_IsCalled_Then_It_Should_Throw_Sum_Exception