|
| 1 | +<!-- Always leave the MS logo --> |
| 2 | + |
| 3 | + |
| 4 | +# SQL Server unit testing with tSQLt, Docker, and GitHub Actions! |
| 5 | + |
| 6 | +This sample describes how to automate the testing for one or more SQL Server objects using tSQLt, Docker, and GitHub Actions! |
| 7 | + |
| 8 | +### Contents |
| 9 | + |
| 10 | +[About this sample](#about-this-sample)<br/> |
| 11 | +[Before you begin](#before-you-begin)<br/> |
| 12 | +[Case history](#case-history)<br/> |
| 13 | +[Run this sample](#run-this-sample)<br/> |
| 14 | +[Sample details](#sample-details)<br/> |
| 15 | +[Disclaimers](#disclaimers)<br/> |
| 16 | +[Related links](#related-links)<br/> |
| 17 | + |
| 18 | +<a name=about-this-sample></a> |
| 19 | + |
| 20 | +## About this sample |
| 21 | + |
| 22 | +- **Applies to:** SQL Server 2016 (or higher) |
| 23 | +- **Key features:** Run SQL Server in Docker containers |
| 24 | +- **Workload:** Unit tests executed on [AdventureWorks](https://github.com/Microsoft/sql-server-samples/releases/tag/adventureworks) |
| 25 | +- **Programming Language:** T-SQL, YAML |
| 26 | +- **Authors:** [Sergio Govoni](https://www.linkedin.com/in/sgovoni/) | [Microsoft MVP Profile](https://mvp.microsoft.com/it-it/PublicProfile/4029181?fullName=Sergio%20Govoni) | [Blog](https://segovoni.medium.com/) | [GitHub](https://github.com/segovoni) | [Twitter](https://twitter.com/segovoni) |
| 27 | + |
| 28 | +<a name=before-you-begin></a> |
| 29 | + |
| 30 | +## Before you begin |
| 31 | + |
| 32 | +To run this example, the following basic concepts are required. |
| 33 | + |
| 34 | +[tSQLt](https://tsqlt.org/) is a unit testing framework for SQL Server. It provides the APIs to create and execute test cases, as well as integrates them with continuous integration servers. The power of the tSQLt framework has been described in my article [The tSQLt framework and the execution of a test](https://segovoni.medium.com/unit-testing-the-tsqlt-framework-and-the-execution-of-a-test-e4d135c3e343). |
| 35 | + |
| 36 | +[Docker](https://www.docker.com/) is one of the most popular systems for running applications in isolable, minimal and easily deployable environments called containers. Since SQL Server 2017, the SQL Server Engine can run in a Docker container, a typical usage of running SQL Server in a Docker container concerns the automation of software tests. |
| 37 | + |
| 38 | +[GitHub Actions](https://github.com/features/actions/) is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline. You can create workflows that build and test every pull request to your repository, or deploy merged pull requests to production. GitHub provides Linux, Windows, and macOS virtual machines to run your workflows, or you can host your own self-hosted runners in your own data center or cloud infrastructure such as Microsoft Azure. |
| 39 | + |
| 40 | +**Software prerequisites:** |
| 41 | + |
| 42 | +1. A GitHub account. If you don't already have a GitHub account, you can get one for free [here](https://github.com/signup)! |
| 43 | + |
| 44 | +<a name=case-history></a> |
| 45 | + |
| 46 | +## Case history |
| 47 | + |
| 48 | +The AdventureWorks database contains the `Production.Product` table that stores products managed and sold by the fake company Adventure Works LTD. |
| 49 | + |
| 50 | +The trigger we have wrote is to prevent the insertion of new products with values less than 10 as a “safety stock”. The Company wishes to always have a warehouse stock of no less than 10 units for each product. The safety stock level is a very important value for the automatic procedures: it allows to re-order materials. The creation of new purchase orders and production orders are based on the safety stock level. To make our trigger simple, it will only respond to the `OnInsert` event, for `INSERT` commands. |
| 51 | + |
| 52 | +<a name=run-this-sample></a> |
| 53 | + |
| 54 | +## Run this sample |
| 55 | + |
| 56 | +<!-- Step by step instructions --> |
| 57 | + |
| 58 | +1. [Sign in](https://github.com/login) to GitHub. If you don't already have an account, [sign up for a new GitHub account](https://docs.github.com/get-started/signing-up-for-github/signing-up-for-a-new-github-account) |
| 59 | +2. Create your sample repository on GitHub, if you've never done it, you can find the guide [here](https://docs.github.com/get-started/quickstart/create-a-repo) |
| 60 | +3. Create a `.github/workflows` directory in your GitHub repository if this directory does not already exist |
| 61 | +4. Copy the [automated-tests.yml](https://github.com/microsoft/sql-server-samples/tree/master/samples/containers/unit-testing/tsqlt-docker/.github/workflows) inside the directory `.github/workflows` you created in the previous step in your repository. The `automated-tests.yml` describes the process that will execute one or more jobs |
| 62 | +5. Create the `source` and `unit-test` directories in the root of your sample repository |
| 63 | +6. Copy all the files located in the [source](https://github.com/microsoft/sql-server-samples/tree/master/samples/containers/unit-testing/tsqlt-docker/source) and [unit-test](https://github.com/microsoft/sql-server-samples/tree/master/samples/containers/unit-testing/tsqlt-docker/unit-test) directories to their respective directories in your repository |
| 64 | +7. View and run the workflow as described [here](https://docs.github.com/actions/quickstart) |
| 65 | +8. Have fun with the solution details outlined below |
| 66 | + |
| 67 | +**The challenge** |
| 68 | + |
| 69 | +The implementation of the trigger and related unit tests has been done, all files are ready in your repository! |
| 70 | + |
| 71 | +The challenge is to automate the execution of the tests at each commit on the main branch of the repository. GitHub Actions is our CI/CD platform, it supports the use of Docker containers and it is intimately integrated into GitHub, the source control that manages our source code. |
| 72 | + |
| 73 | +**Understand and manage your first workflow** |
| 74 | + |
| 75 | +Workflows are defined with a YAML file stored in the same repository which holds the source code. The workflows will be triggered when an event occurs in the repository. Anyway, a workflow can also be activated manually or according to a defined schedule. |
| 76 | + |
| 77 | +A sample YAML file that implements the test automation workflow is already in your sample repository, the fundamental steps of the process are: |
| 78 | + |
| 79 | +1. Definition of activation events |
| 80 | +2. Creating a Docker container from a SQL Server image on Linux |
| 81 | +3. AdventureWorks database recovery |
| 82 | +4. Installation of the tSQLt framework |
| 83 | +5. Creating the database objects to be tested (SUT) |
| 84 | +6. Creation and execution unit tests |
| 85 | + |
| 86 | +**1. Definition of activation events** |
| 87 | + |
| 88 | +The definition of the activation events is typically done at the beginning of the YAML script with a code snippet similar to the one shown below. The workflow is activated when push or pull request events occur on the `master` branch. The `workflow_dispatch` specification allows you to run the workflow manually from the actions tab. |
| 89 | + |
| 90 | +``` |
| 91 | +# Controls when the workflow will run |
| 92 | +on: |
| 93 | + # Triggers the workflow on push or pull request events but only for the "master" branch |
| 94 | + push: |
| 95 | + branches: [ "master" ] |
| 96 | + pull_request: |
| 97 | + branches: [ "master" ] |
| 98 | + |
| 99 | + # Allows you to run this workflow manually from the Actions tab |
| 100 | + workflow_dispatch: |
| 101 | +``` |
| 102 | + |
| 103 | +**2. Creating a Docker container from a SQL Server image on Linux** |
| 104 | + |
| 105 | +Creating a Docker container from a SQL Server image on Linux can be done by requesting the `sqlserver service` accompanied by the path to the Docker image you want to use. |
| 106 | + |
| 107 | +The official images provided by Microsoft for SQL Server on Linux are available [here](https://hub.docker.com/_/microsoft-mssql-server). |
| 108 | + |
| 109 | +We will not use an official image downloaded from the Microsoft registry. We will use a Docker image of SQL Server with the AdventureWorks database installed, this image is published by [chriseaton](https://hub.docker.com/u/chriseaton), you can find it at this [link](https://hub.docker.com/r/chriseaton/adventureworks). |
| 110 | + |
| 111 | +The following YAML code snippet sets up the SQL Server service. |
| 112 | + |
| 113 | +``` |
| 114 | +jobs: |
| 115 | + windows-auth-tsqlt: |
| 116 | + name: Installting tSQLt with SQL Auth |
| 117 | + # The type of runner that the job will run on |
| 118 | + runs-on: ubuntu-latest |
| 119 | + |
| 120 | + services: |
| 121 | + sqlserver: |
| 122 | + image: chriseaton/adventureworks:latest |
| 123 | + ports: |
| 124 | + - 1433:1433 |
| 125 | + env: |
| 126 | + ACCEPT_EULA: Y |
| 127 | + SA_PASSWORD: 3uuiCaKxfbForrK |
| 128 | +``` |
| 129 | + |
| 130 | +In order to reference the newly created Docker container it is important to save its identifier in an environment variable. |
| 131 | + |
| 132 | +The following snippet of YAML code sets the `ENV_CONTAINER_ID` variable with the ID of the container created. |
| 133 | + |
| 134 | +``` |
| 135 | +- name: Set environment variable ENV_CONTAINER_ID |
| 136 | + run: echo "ENV_CONTAINER_ID=$(docker ps --all --filter status=running --no-trunc --format "{{.ID}}")" >> $GITHUB_ENV |
| 137 | +``` |
| 138 | + |
| 139 | +**3. AdventureWorks database recovery** |
| 140 | + |
| 141 | +The AdventureWorks database recovery can be performed using the following docker exec command. |
| 142 | + |
| 143 | +``` |
| 144 | +- name: Restore AdventureWorks |
| 145 | + run: docker exec -i $ENV_CONTAINER_ID /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "3uuiCaKxfbForrK" -Q "RESTORE DATABASE [AdventureWorks2017] FROM DISK = '/adventureworks.bak' WITH MOVE 'AdventureWorks2017' TO '/var/opt/mssql/data/AdventureWorks.mdf', MOVE 'AdventureWorks2017_log' TO '/var/opt/mssql/data/AdventureWorks_log.ldf'" |
| 146 | +``` |
| 147 | + |
| 148 | +**4. Installation of the tSQLt framework** |
| 149 | + |
| 150 | +The installation of the latest version of tSQLt framework in the AdventureWorks database is done using the GitHub Actions tSQLt Installer published by [lowlydba](https://github.com/lowlydba), you can find more details [here](https://github.com/lowlydba/tsqlt-installer) and on the [GitHub Actions marketplace](https://github.com/marketplace/actions/tsqlt-installer). |
| 151 | + |
| 152 | +The snippet of YAML code used for the installation of the tSQLt framework in the AdventureWorks database is the following. |
| 153 | + |
| 154 | +``` |
| 155 | +steps: |
| 156 | + - uses: actions/checkout@v2 |
| 157 | + - name: Install tSQLt with SQL auth on AdventureWorks2017 |
| 158 | + uses: lowlydba/tsqlt-installer@v1 |
| 159 | + with: |
| 160 | + sql-instance: localhost |
| 161 | + database: AdventureWorks2017 |
| 162 | + version: latest |
| 163 | + user: sa |
| 164 | + password: 3uuiCaKxfbForrK |
| 165 | +``` |
| 166 | + |
| 167 | +**5. Creating the database objects to be tested (SUT)** |
| 168 | + |
| 169 | +The test environment is ready, we have a SQL Server instance on Linux inside a Docker container; the AdventureWorks database has been restored and it is ready for use. |
| 170 | + |
| 171 | +Let's go ahead with the creation of the trigger and the stored procedure (that manages errors), they represent our [System Under Test (SUT)](https://en.wikipedia.org/wiki/System_under_test). |
| 172 | + |
| 173 | +The TR_Product_SafetyStockLevel trigger creation script and the usp_Raiserror_SafetyStockLevel stored procedure creation script are saved in the source directory of this sample. |
| 174 | + |
| 175 | +Triggers and stored procedures are created in the AdventureWorks database attached to the SQL Server instance, the YAML code snippet that performs this operation is the following. |
| 176 | + |
| 177 | +``` |
| 178 | +- name: Create sp usp_Raiserror_SafetyStockLevel |
| 179 | + run: docker exec -i $ENV_CONTAINER_ID /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "3uuiCaKxfbForrK" -d AdventureWorks2017 -b < ./source/usp-raiserror-safetystocklevel.sql |
| 180 | +- name: Create TR_Product_SafetyStockLevel |
| 181 | + run: docker exec -i $ENV_CONTAINER_ID /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "3uuiCaKxfbForrK" -d AdventureWorks2017 -b < ./source/tr_product_safetystocklevel.sql |
| 182 | +``` |
| 183 | + |
| 184 | +**6. Creation and execution of test units** |
| 185 | + |
| 186 | +The last phase of this workflow is represented by the creation and execution of the unit tests. |
| 187 | + |
| 188 | +The test class and the unit tests creation scripts are contained in the unit-test subfolder of this sample. |
| 189 | + |
| 190 | +Let's go ahead with the creation of the test class dedicated to the TR_Product_SafetyStockLevel trigger, we called it UnitTestTRProductSafetyStockLevel. |
| 191 | + |
| 192 | +The following docker exec command, that uses sqlcmd, executes the TSQL commands contained in the test-class-trproductsafetystocklevel.sql script. |
| 193 | + |
| 194 | +``` |
| 195 | +- name: Create test class UnitTestTRProductSafetyStockLevel |
| 196 | + run: docker exec -i $ENV_CONTAINER_ID /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "3uuiCaKxfbForrK" -d AdventureWorks2017 -b < ./unit-test/test-class-trproductsafetystocklevel.sql |
| 197 | +``` |
| 198 | + |
| 199 | +Let's go ahead with the creation and execution of the unit tests. Each .sql file of the test case family contains the TSQL commands for creating and running the related unit test. Each store procedure tests one and only one test case. |
| 200 | + |
| 201 | +The following snippet of YAML code creates and runs the test units. |
| 202 | + |
| 203 | +``` |
| 204 | +- name: Create and run test case try to insert one wrong row |
| 205 | + run: docker exec -i $ENV_CONTAINER_ID /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "3uuiCaKxfbForrK" -d AdventureWorks2017 -b < ./unit-test/test-case-try-to-insert-one-wrong-row.sql |
| 206 | +- name: Create and run test case try to insert one right row |
| 207 | + run: docker exec -i $ENV_CONTAINER_ID /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "3uuiCaKxfbForrK" -d AdventureWorks2017 -b < ./unit-test/test-case-try-to-insert-one-right-row.sql |
| 208 | +- name: Create and run test case try to insert multiple rows |
| 209 | + run: docker exec -i $ENV_CONTAINER_ID /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "3uuiCaKxfbForrK" -d AdventureWorks2017 -b < ./unit-test/test-case-try-to-insert-multiple-rows.sql |
| 210 | +- name: Create and run test case try to insert multiple rows ordered |
| 211 | + run: docker exec -i $ENV_CONTAINER_ID /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P "3uuiCaKxfbForrK" -d AdventureWorks2017 -b < ./unit-test/test-case-try-to-insert-multiple-rows-ordered.sql |
| 212 | +``` |
| 213 | + |
| 214 | +The YAML script for our workflow is complete, you can find it here. |
| 215 | + |
| 216 | +<a name=sample-details></a> |
| 217 | + |
| 218 | +## Sample details |
| 219 | + |
| 220 | +Unit tests developed for a SQL Server solution are not just meant to verify that requirements have been met once, prior to release; the real game changer is represented by the possibility of repeating the checks during the development of new code and during the bug fixing process. |
| 221 | + |
| 222 | +The repeatability of the tests provides the ability to automate them, an essential condition for integrating automatic tests within a Continuous Integration platform. In this article we described how to automate the testing of SQL Server objects using tSQLt, Docker and GitHub Actions! |
| 223 | + |
| 224 | +<a name=disclaimers></a> |
| 225 | + |
| 226 | +## Disclaimers |
| 227 | + |
| 228 | +The code included in this sample is not intended to be a set of best practices on how to build scalable enterprise grade unit testing or CI/CD system. This is beyond the scope of this quick start sample. |
| 229 | + |
| 230 | +<a name=related-links></a> |
| 231 | + |
| 232 | +## Related Links |
| 233 | +<!-- Links to more articles. Remember to delete "en-us" from the link path. --> |
| 234 | + |
| 235 | +For more information, see these articles: |
| 236 | + |
| 237 | +- [Unit testing: What it is and why it is important for T-SQL code!](https://medium.com/@segovoni/unit-testing-what-it-is-and-why-it-is-important-for-t-sql-code-7e9df7ca8bfe) |
| 238 | +- [Unit testing: The tSQLt framework and the execution of a test!](https://segovoni.medium.com/unit-testing-the-tsqlt-framework-and-the-execution-of-a-test-e4d135c3e343) |
| 239 | +- [Unit testing: How to write your first unit test for T-SQL code](https://segovoni.medium.com/unit-testing-how-to-write-your-first-unit-test-for-t-sql-code-3bc1533acbbc) |
0 commit comments