Software testing
Software testing is the art of measuring and maintaining software quality to ensure that the user expectations and requirements, business values, non-functional requirements (such as security, reliability, and recoverability), and operational policies are all met. Testing is a team effort to accomplish the well-understood and agreed-upon minimum quality bar and definition of “done”.
Definition of Done (DOD): is a team definition and commitment to quality to be applied to the solution during each iteration (this may also occur at the Task or User Story level, too). Consider the design, code reviews, refactoring, and testing when discussing and defining your definition of done.
Understanding and Adapting TDD
TDD is a happening term in the industry these days, especially among those software development organizations that are practicing the Agile development methodology.
TDD is an evolutionary approach and mindset towards software development that enforces writing Unit Tests as you are coding the functionality or feature.
TDD gives an opportunity to think as a “user of the code” instead of an “implementer of the code”. Eventually, TDD helps in better design, quality and test coverage.
What is the Unit Test
An ideal unit test is a piece of code written by a developer that exercises a small but specific area of code functionality to ensure that it works as expected and can be tested in isolation from other units.
Why Unit Test
According to the Test Triangle, in a software project, the largest number of tests must be Unit Tests. Because multiple individual Unit Tests contribute to Integration Testing and so on.
The image below also depicts that, over the period of time of a project, a Unit Test needs to be continuously written and run for software quality.
Benefits of TDD
Unit testing helps in various ways as in the following:
- Reduce bugs by identifying all the use case scenarios to reflect intent (end user's mindset, business needs, expected functionality, and business validations, and so on).
- Less-and-less time on debugging.
- Avoid collateral damage, in other words, a fix in one area may break functionality in another possibly related/unrelated area.
- Helps you achieve YAGNI that is "You Aren't Gonna Need It". In other words, saves you from writing code functionality that you don't need.
But I am a Developer, Not a Tester
First, creating a Unit Test is the developer's responsibility. Agreed, developers are not testers and that is true of testers too. But thinking of the most common scenarios worth testing that might cause failure to your code functionality is all that you need to code in your Unit Tests.
Got it, but I don't have a Testing Mind Set
Not having a Testing Mind Set is actually a genuine issue and this happens because developers usually never think of testing their code until it's deployed to QA or production. To overcome this issue you must pair with a QA Engineer or Software Development Engineer in Test (SDET) in your team. You must write all the possible test areas that you think of and get it reviewed.
Career Tip: In today's software world, a unit testing skill is a major requirement for any developer, lead, or architect position.
Requirements for other types of testing as well
Do I need to do other types of testing as well? From a developer's point of view, the short and straight answer is
no, but many teams and organizations require their developers to write even Integration, Load, and Stress Tests as well. A dedicated QA/Test team is usually responsible for doing a non-unit type of testing, whether it is manual or automated.
Career Tip: Educate yourself about other types of testing.
System Under Test (SUT)
System Under Test (SUT) is the system that will be tested by Unit Tests for code accuracy and possible scenarios that might either break the functionality at runtime or not produce legitimate results.
Assume you are working on a Bank Application's Business Logic (BankApplication.dll) and your code looks as in this:
- namespace BankApplication.Savings
- {
- public class Account
- {
- private double accountBalance = 0.00;
- private bool accountStatus = false;
-
- private enum Roles
- {
- Customer,
- Manager
- }
- public bool IsAccountActive(string accNumber)
- {
- return accountStatus;
- }
-
- public double Balance(string accNumber)
- {
- return accountBalance;
- }
-
- public double Deposit(string accNumber, double amount)
- {
- return accountBalance = accountBalance + amount;
- }
-
- public double Withdwral(string accNumber, double amount)
- {
- return accountBalance = accountBalance - amount;
- }
-
- private bool ActivateAccount(string accNumber, Roles userRole)
- {
- accountStatus = true;
- return accountStatus;
- }
- }
- }
This Bank Application's DLL should work properly, assuming all the right data and parameters are provided. But when you start thinking from a Test Driven Development (TDD) perspective and you put the DLL under test then you start detecting the areas of further improvement and refactoring.
What to Unit Test
But what do I unit test? the most critical and common area of Unit Testing can be categorized under Boundary and Error Conditions.
Boundary Conditions
Boundary conditions are very critical to the success of any software code ever written. All your business validation/rules are actually a candidate for Boundary Unit Tests.
Identifying boundary conditions is very important for unit testing, such as:
- Empty or missing values (such as 0, "", or null).
- Inappropriate values that are not realistic from a business point of view, such as a person's age of -1 or 200 years or so.
- DOB is tomorrow's date or time in the future.
- Duplicates in lists that shouldn't have duplicates.
- The password is the same as either First name or Last name
- Special characters or case related conditions.
- Formatting of data, for example, the name must be capitalized. For example Vidya Vrat, Agarwal.
- Type of acceptable values in a field. For example, the name can't hold a numeric, and age can't hold letters.
- Range is another critical thing to test and it's often coded as business validation rules.
Error Conditions
Building a real-world application causes real-world errors at run-time and errors do happen. Hence, you should be able to test that your code handles all such errors, for example, think of the following scenarios:
- Can't handle DivideByZeroException
- Consider the scenario of AccessDenied
- Don't ignore NullReferenceExceptions
- Check for existence; FileNotFoundException, DirectoryNotFoundException and so on
Properties of a Good Unit Test
Units Tests are very simple and usually small C# code segments, but there are a few criteria that can really define what a good Unit Test is. Here are the properties that a good Unit Test must have:
Automatic: Each Test must “Automatically” exercise small functionality in terms of invoking the test and verifying the results.
Thorough: Unit Tests are supposed to test all the possible areas of functionality that are subject to failure due to incorrect input.
Repeatable: Unit Tests must be repeatable for every build and must produce the same results. The development best practice suggests that if you are working on code that is impacting a Unit Test then you must fix the affected Unit Test as well and ensure that it passes.
Independent: Unit Tests must be independent of another test. In other words, no collateral damage. Hence, a Unit Test must focus only on a small aspect of big functionality. When this Unit Test fails, it should be easy to discover where the issue is in the code.
Professional: Even though at times Unit Tests may appear to be very simple and small, you must write Unit Tests with coding practices as good as you use for your main development coding. You may want to follow Refactoring, Code Analysis, and Code Review practices, and so on as for your Test Projects as well.
Structure of a Unit Test - Arrange, Act and Assert
An ideal unit test code is divided into the following three main sections:
- Arrange: Set up all conditions needed for testing (create any required objects, allocate any needed resources, and so on).
- Invoke the method to be tested with possible parameters or values.
- Assert: Verify that the tested method returns the output as expected.
Let's follow TDD
From the SUT we have, let's focus on a piece of production code.
- private bool accountStatus = false;
-
- public bool IsAccountActive(string accNumber)
- {
- return accountStatus;
- }
The IsAccountActive() function is supposed to return the account status of a provided account #. We will be writing tests to verify the behavior and then fixing the production code to make the Tests Pass and repeat the same with more scenarios and possibilities (use case, business rules/validations, and so on.)
Scenario #1: To test this production code we add [TestMethod] TestAccountStatus_Active_Success() to our TDD Suite (Unit Test Project).
- [TestMethod]
- public void TestAccountStatus_Active_Success()
- {
- Assert.IsTrue(obj.IsAccountActive("1234"),"Failed Account is not Active");
- }
The test failed, because we have not yet implemented the production code.
Let's implement the IsAccountActive() in the code to make the test pass. New code additions are:
- public bool IsAccountActive(string accNumber)
- {
- if (accNumber != null)
- {
- accountStatus = true;
- }
- else
- {
- accountStatus = false;
- }
-
- return accountStatus;
- }
Repeat it with more validation and business rules
What would be the response from the caller of the API if null is passed as an account number? Let's say the API must throw an ArgumentException.
Scenario #2: To verify this behavior add a Test First and we will use the Microsoft Test provided attribute [ExpectedException(typeof(ArgumentException))] to verify the exception the caller will be expecting in a real situation.
-
- [TestMethod][Priority(0)]
- [ExpectedException(typeof(ArgumentException))]
- public void TestAccountStatus_ArgumentException_Success()
- {
- obj.IsAccountActive(null);
- }
Since our production or development code is not yet ready to handle this scenario, this test is supposed to fail and the reason is shown on the right-side panel (I underlined in Green).
Refactor-Test and Test Further More
Let's refactor our production or development code to make the TestAccountStatus_ArgumentException_Success() test case pass. The new code additions are highlighted in Green.
- public bool IsAccountActive(string accNumber)
- {
- if (accNumber != null)
- {
- accountStatus = true;
- }
- else
- if (accNumber == null)
- {
- throw new ArgumentException("Account number Can't be Null");
- }
- else
- {
- accountStatus = false;
- }
-
- return accountStatus;
- }
Now let's re-run the test TestAccountStatus_ArgumentException_Success() and observe the results.
Scenario #3: Now let's verify the behavior if we pass White Space " " as an account number and expect an ArgumentException is thrown. We will use
the [ExpectedException(typeof(ArgumentException))] attribute.
- [TestMethod] [Priority(0)]
- [ExpectedException(typeof(ArgumentException))]
- public void TestAccountStatus_AccountNumberWhiteSpace_ArgumentException_Success()
- {
- obj.IsAccountActive(" ");
- }
Run the test and you will see the result as shown in the image below:
Let's refactor the code and it will look as shown below. The new code additions are:
- public bool IsAccountActive(string accNumber)
- {
- if (accNumber != null)
- {
- accountStatus = true;
- }
- else
- if (String.IsNullOrWhiteSpace(accNumber))
- {
- throw new ArgumentException("Account number Can't be Null or have
- White Spaces");
- }
- else
- {
- accountStatus = false;
- }
-
- return accountStatus;
- }
Let's re-run the test and observe the results:
The test still fails, though we added the right condition to verify WhiteSpace.
Let's Debug the Test
Add a breakpoint to the failing test at the line where you are invoking the production code as in the following:
Right-click in the code editor and select Debug Tests as shown in the image below.
This will start Test Execution in Debug mode and you will see a screen as shown below:
Press F11 and you will be taken to the Production code to debug it further.
Press F10 to Step-Over and keep pressing F10 until the Debug session ends and you will observe the flow as shown below:
So what we discovered is that the first condition accNumber!= null is true even if WhiteSpace has been passed as the account number. Hence, it's another opportunity to refactor the production code and re-run the Test(s). The new code additions are:
- public bool IsAccountActive(string accNumber)
- {
- if (String.IsNullOrWhiteSpace(accNumber))
- {
- throw new ArgumentException("Account number Can't be Null or have
- White Spaces");
- }
- else
- {
- accountStatus = true;
- }
-
- return accountStatus;
- }
Test Often
After each Test addition or refactoring session, re-run the tests in the Test Explorer to ensure that previously passing tests haven't begun failing, in other words, there is no Collateral Damage.
YouTube Video