Art of Isolating Dependencies and Data in Database Unit Testing

All the database developers more or less write database unit tests that not only help in detecting bugs early but also save a lot of time and efforts when the unexpected behavior of database objects becomes a production issue.

Nowadays, there are a number of database unit testing frameworks such as tSQLt along with third-party unit testing tools including dbForge Unit Test.

On the one hand, the benefit of using third-party testing tools is that the development team can instantly create and run unit tests with added features. Also, using a testing framework directly gives you more control over the unit tests. Therefore, you can add more functionality to the unit testing framework itself. However, in this case, your team must have time and a certain level of expertise to do this.

This article explores some standard practices that can help us to improve the way we write database unit tests.

First, let us go through some key concepts of database unit testing.

What is Database Unit Testing

According to Dave Green, database unit tests ensure that small units of the database, such as tables, views, stored procedures, etc., are working as expected.

Database unit tests are written to verify whether the code meets business requirements.

For example, if you receive a requirement such as “A librarian (end-user) should be able to add new books to the library (Management Information System)”, you need to think of applying unit tests for the stored procedure to check if it can add a new book to the Book table.

Sometimes, a series of unit tests ensure that the code meets the requirements. Therefore, most of the unit testing frameworks including tSQLt allow grouping related unit tests into a single test class rather than running individual tests.

AAA Principle

It is worth mentioning about the 3-step principle of unit testing which is a standard practice to write unit tests. The AAA principle is the basis for unit testing and consists of the following steps:

  1. Arrange/Assemble
  2. Act
  3. Assert

The Arrange section is the first step in writing database unit tests. It guides through configuring a database object for testing and setting up the expected results.

The Act section is when a database object (under test) is called to produce the actual output.

The Assert step deals with matching the actual output to the expected one and verifies whether the test either passes or fails.

Let’s explore these methods on particular examples.

If we create a unit test to verify that the AddProduct stored procedure can add a new product, we set up the Product and ExpectedProduct tables after the product is added. In this case, the method comes under the Arrange/Assemble section.

Calling the AddProduct procedure and putting the result into the Product table is covered by the Act section.

The Assert part simply matches the Product table with the ExpectedProduct table to see whether the stored procedure has been executed successfully or failed.

Understanding Dependencies in Unit Testing

So far, we have discussed basics of database unit testing and importance of the AAA (Assemble, Act, and Assert) principle when creating a standard unit test.

Now, let us focus on another important piece of the puzzle – dependencies in unit testing.

Apart from following the AAA principle and focusing only on a particular database object (under test), we also need to know the dependencies that may affect unit tests.

The best way to understand dependencies is to look at an example of a unit test.

EmployeesSample Database Setup

To move on, create a sample database and call it EmployeesSample:

Now, create the Employee table in the sample database:

Populating Sample Data

Populate the table by adding few records:

The table looks like this:

Please note that I am using dbForge Studio for SQL Server in this article. Thus, the output look may differ if you run the same code in SSMS (SQL Server Management Studio). There is no difference when it comes to scripts and their results.

Requirement to Add New Employee

Now, if a requirement to add a new employee has been received, the best way to meet the requirement is to create a stored procedure which can successfully add a new employee to the table.

To do this, create the AddEmployee stored procedure as follows:

Unit Test to Verify whether the Requirement is Met

We are going to write a database unit test to verify if the AddEmployee stored procedure meets the requirement to add a new record to the Employee table.

Let us focus on understanding the unit test philosophy by simulating a unit test code rather than writing a unit test with a testing framework or third-party unit testing tool.

Simulating Unit Test and Applying AAA Principle in SQL

The first thing we need to do is to imitate the AAA principle in SQL since we are not going to use any unit testing framework.

The Assemble section is applied when the actual and expected tables are normally set up along with the expected table getting populated. We can make use of SQL variables to initialize the expected table in this step.

The Act section is used when the actual stored procedure is called to insert data into the actual table.

The Assert section is when the expected table matches the actual table. Simulating the Assert part is a little bit tricky and can be achieved by the following steps:

  • Counting the common (matching) rows between two tables which should be 1 (since the expected table has just one record that should match the actual table)
  • Excluding the actual table records from the expected table records should equal 0 (if the record in the expected table exists in the actual table as well, then excluding all the actual table records from the expected table should return 0)

The SQL script is as follows:

Code

Running Simulated Unit Test

After the stored procedure is created, execute it with the simulated unit test:

The output is as follows:

Congratulations! The database unit test passed.

Identifying Issues in the form of Dependencies in Unit Test

Can we detect anything wrong in the unit test we created despite the fact that it has been written and run successfully?

If we closely look at the unit test setup (the Assemble part), the expected table has an unnecessary binding with the identity column:

Before writing a unit test we have already added 5 records to the actual (Employee) table. Thus, at the test setup, the identity column for the expected table begins with 6. However, this means that we always expect 5 records to be in the actual (Employee) table to match it with the expected table (#EmployeeExpected).

In order to understand how this may affect the unit test, let us have a look at the actual (Employee) table now:

Add another record to the Employee table:

Have a look at the Employee table now:

Delete EmpoyeeId 6 (Adil) so that the unit test can run against its own version of EmployeeId 6 (Adil) rather than the previously stored record.

Run the simulated unit test and see the results:

The test has failed this time. The answer lies in the Employee table result set as shown below:

The Employee Id binding in the unit test as mentioned above does not work when we re-run the unit test after adding a new record and deleting the previously added employee record.

There are three types of dependencies in the test:

  1. Data Dependency
  2. Key Constraint Dependency
  3. Identity Column Dependency

Data Dependency

First of all, this unit test depends on data in the database. According to Dave Green, when it comes to the unit testing database, the data itself is a dependency.

This means that your database unit test should not rely on the data in the database. For example, your unit test should contain the actual data to be inserted into the database object (table) rather than rely on the data already existing in the database which can be deleted or modified.

In our case, the fact that five records have already been inserted into the actual Employee table is a data dependency which must be prevented because we should not violate the philosophy of unit test that says only the unit of the code is tested.

In other words, test data should not rely on the actual data in the database.

Key Constraint Dependency

Another dependency is a key constraint dependency which means that the primary key column EmployeeId is also a dependency. It must be prevented in order to write a good unit test. However, a separate unit test is required to test a primary key constraint.

For example, in order to test the AddEmployee stored procedure, the Employee table primary key should be removed so that an object can be tested without the worry of violating a primary key.

Identity Column Dependency

Just like a primary key constraint, the identity column is also a dependency. Thus, there is no need in testing the identity column auto-increment logic for the AddEmployee procedure; it must be avoided at any cost.

Isolating Dependencies in Unit Testing

We can prevent all three dependencies by removing the constraints from the table temporarily and then do not depend on the data in the database for the unit test. This is how the standard database unit tests are written.

In this case, one might ask where the data for the Employee table came from. The answer is that the table gets populated with test data defined in the unit test.

Altering Unit Test Stored Procedure

Let us now remove the dependencies in our unit test:

Code

Running Dependency-Free Simulated Unit Test

Run the simulated unit test to see the results:

Rerun the unit test to check the AddEmployee stored procedure:

Congratulations! Dependencies from the unit test have been removed successfully.

Now, even if we add a new record or set of new records to the Employee table, it is not going to affect our unit test since we have removed the data and constraint dependencies from the test successfully.

Creating Database Unit Test Using tSQLt

The next step is to create a real database unit test based on the simulated unit test.

If you are using SSMS (SQL Server Management Studio) you will have to install the tSQLt framework, create a test class and enable CLR before writing and running the unit test.

If you are using dbForge Studio for SQL Server you can create the unit test by right-clicking the AddEmployee stored procedure and then clicking “Unit Test” => “Add New Test…” as shown below:

To add a new test, fill in the required unit test information:

To write the unit test, use the following script:

Then, run the database unit test:

Congratulations! We have successfully created and run database unit test which is free from dependencies.

Things to Do

That’s it. You are ready to isolate dependencies from database unit tests and create database unit test free from data and constraint dependencies after going through this article. As a result, you can improve your skills performing the following things:

  1. Please try adding the Delete Employee stored procedure and create a simulated database unit test for Delete Employee with dependencies to see if it fails under certain conditions
  2. Please try adding the Delete Employee stored procedure and create a database unit test free from dependencies to see if an employee can be deleted
  3. Please try adding the Search Employee stored procedure and create a simulated database unit test with dependencies to see if an employee can be searched for
  4. Please try adding the Search Employee stored procedure and create a database unit test free from dependencies to see if an employee can be searched for
  5. Please try more complex requirements by creating stored procedures to meet the requirements and then writing database unit tests free from dependencies to see if they pass the test or fail. However, please make sure that the test is repeatable and focused on testing the unit of the code
Haroon Ashraf

Haroon Ashraf

Haroon's deep interest in logic and reasoning at an early age of his academic career paved his path to become a data professional.

He began his professional life as a computer programmer more than 10 years ago working on his first data venture to migrate and rewrite a public sector database driven examination system from IBM AS400 (DB2) to SQL Server 2000 using VB 6.0 and Classic ASP along with developing reports and archiving many years of data.

His work and interest revolves around Database-Centric Architectures and his expertise include database and reports design, development, testing, implementation and migration along with Database Life Cycle Management (DLM).
Haroon Ashraf

Haroon Ashraf

Haroon's deep interest in logic and reasoning at an early age of his academic career paved his path to become a data professional. He began his professional life as a computer programmer more than 10 years ago working on his first data venture to migrate and rewrite a public sector database driven examination system from IBM AS400 (DB2) to SQL Server 2000 using VB 6.0 and Classic ASP along with developing reports and archiving many years of data. His work and interest revolves around Database-Centric Architectures and his expertise include database and reports design, development, testing, implementation and migration along with Database Life Cycle Management (DLM).