tSQLt 数据库的单元测试框架

单元测试在软件开发中很常见, 其特点是不侵入现有代码但能和现有代码融为一体,覆盖到每一个功能单元.

在数据库领域好的测试框架非常少见, tSQLt正是为数不多的其中之一. tSQLt 继承了单元测试的理念, 比如平时我们要测试数据库的时候, 数据库的表往往有很多外键关联, 如果要为测试添加测试数据, 就必须加入很多不必要的数据以满足外键关联或者把外键去掉, 很不方便. tSQLt提供一个FakeTable的存储过程, 可以创建一个不带外键和constraint的假表, 但表结构和名字与原表一致(原表在测试过程中会被重命名, 测试跑完会恢复名字), 用于测试.

tSQLt还提供了很多Assert开头的存储过程:


用于各种类型的比较. 比如要比较两个表的内容是否一致, 可以用AssertEqualsTable, 值得一提的是, tSQLt会根据列名来比较, 被比较的两表的列的顺序可以不一样, 甚至列的类型也可以不同, 比如 Bigint 和 int 在测试中可以比较, 只要数值一样就算Equal.

0. 使用 NewTestClass创建一个新的schema, schema就想当于编程语言单元测试里的class, 后续的具体测试的SP可以归于这个schema, tSQLt会自动为这个schema添加一个external property表名它代表一类测试.可以在一个view([tSQLt].[TestClasses])里查看 .

exec tSQLt.NewTestClass 'testclass'

1. 新建一个存储过程, 在这个存储过程中按需调用 FakeTable 和 Assert系列的SP 来定义 test case, 例如:

create procedure testclass.test1 as 
begin
EXEC tSQLt.AssertEqualsTable 'tb1', 'tb2'
end;

2. 然后使用 tSQLt.Run 这个SP 来执行比较, tSQLt.Run可以指明要跑的test case的SP名字(schema.spname), 也可以通过要跑的test cases的schema名字来批量运行. 结果会输出到 [tSQLt].[TestResult] 这表. 此外还有一个 tSQLt.RunAll 来跑所有cases.


在底层, tSQLt 会调用一个CLR dll 的assembly来比较, 可以在tSQLt的安装脚本里找到如下一句:

CREATE ASSEMBLY [tSQLtCLR] AUTHORIZATION [dbo] FROM 0x4D5A90000300000004000000FFFF0000B800000000000000400000000000000000000000000000000000000000000000000.....
其实就是把dll的编码直接用SQL语句注入到sqlserver了, 为了使用CLR的assembly, 必须开启CLR支持功能

EXEC sp_configure 'clr enabled', 1;
最后所有的test case 都会在事务里执行, tSQLt 巧妙地利用了事务的回滚 做到测试完成后, 一点痕迹都不留下.


附录官网的英文教程:

TSQLT TUTORIAL

Welcome to tSQLt, a unit testing framework for the T-SQL language for Microsoft SQL Server 2005 and beyond. tSQLt takes database unit testing a step further by providing methods to help isolate functionality to be tested. Before diving in to the isolation features, however, let’s start with a simple example.

Example 1: AssertEquals
Suppose we are writing a function which calculates the result of a currency exchange. Our function will take two parameters, the amount to be exchanged and the exchange rate. Our test is as follows:

EXEC tSQLt.NewTestClass 'testFinancialApp';
GO

CREATE PROCEDURE testFinancialApp.[test that ConvertCurrency converts using given conversion rate]
AS
BEGIN
    DECLARE @actual MONEY;
    DECLARE @rate DECIMAL(10,4); SET @rate = 1.2;
    DECLARE @amount MONEY; SET @amount = 2.00;

    SELECT @actual = FinancialApp.ConvertCurrency(@rate, @amount);

    DECLARE @expected MONEY; SET @expected = 2.4;   --(rate * amount)
    EXEC tSQLt.AssertEquals @expected, @actual;

END;
GO

Note, that we first use tSQLt.NewTestClass to create a new schema for our test cases. This allows us to organize our tests and execute them as a group, which we will call a test class. A test class can be executed as follows:

EXEC tSQLt.Run 'testFinancialApp';

Our test procedure calls the ConvertCurrency procedure (in the FinancialApp schema), passing the exchange rate and amount. The return value is retrieved and compared against the expected value in the tSQLt.AssertEquals method.

Example 2: FakeTable

Suppose we want to test a procedure that reads data from a table. We need to put data in this table to properly do the test. However, the table might have constraints such as checks and foreign keys that would make it difficult to put data in the table just for a test case. Instead of creating a lot of unnecessary data, we can replace the table by calling fakeTable. This will recreate the original table without constraints.

So suppose we want to test a more advanced currency conversion method that looks up the exchange rate from a CurrencyConversion table:

CREATE PROCEDURE testFinancialApp.[test that ConvertCurrencyUsingLookup converts using conversion rate in CurrencyConversion table]
AS
BEGIN
    DECLARE @expected MONEY; SET @expected = 3.2;
    DECLARE @actual MONEY;
    DECLARE @amount MONEY; SET @amount = 2.00;
    DECLARE @sourceCurrency CHAR(3); SET @sourceCurrency = 'EUR';
    DECLARE @destCurrency CHAR(3); SET @destCurrency = 'USD';

------Fake Table
    EXEC tSQLt.FakeTable 'FinancialApp', 'CurrencyConversion';

    INSERT INTO FinancialApp.CurrencyConversion (id, SourceCurrency, DestCurrency, ConversionRate)
                                         VALUES (1, @sourceCurrency, @destCurrency, 1.6);
------Execution
    SELECT @actual = amount FROM FinancialApp.ConvertCurrencyUsingLookup(@sourceCurrency, @destCurrency, @amount);

------Assertion
    EXEC tSQLt.AssertEquals @expected, @actual;
END;
GO

FakeTable takes two parameters, the schema and the table name to be faked. Because every test in tSQLt operates inside of a transaction, the original table is put back in place after the test finishes. FakeTable has allowed us to isolate the testing of the procedure independently of the rest of the database’s constraints.

Example 3: AssertEqualsTable

To simplify the comparison of tables and resultsets, tSQLt introduces a table comparison feature: tSQLt.AssertEqualsTable.

In this example, we want to generate a sales report. Our sales table tracks the amount of sale in the original currency, but since we want consistent data we should use our currency converter to make sure the report shows everything in a single currency.

CREATE PROCEDURE testFinancialApp.[test that Report gets sales data with converted currency]
AS
BEGIN
    IF OBJECT_ID('actual') IS NOT NULL DROP TABLE actual;
    IF OBJECT_ID('expected') IS NOT NULL DROP TABLE expected;


------Fake Table
    EXEC tSQLt.FakeTable 'FinancialApp', 'CurrencyConversion';
    EXEC tSQLt.FakeTable 'FinancialApp', 'Sales';

    INSERT INTO FinancialApp.CurrencyConversion (id, SourceCurrency, DestCurrency, ConversionRate)
                                         VALUES (1, 'EUR', 'USD', 1.6);
    INSERT INTO FinancialApp.CurrencyConversion (id, SourceCurrency, DestCurrency, ConversionRate)
                                         VALUES (2, 'GBP', 'USD', 1.2);

    INSERT INTO FinancialApp.Sales (id, amount, currency, customerId, employeeId, itemId, date)
                                         VALUES (1, '1050.00', 'GBP', 1000, 7, 34, '1/1/2007');
    INSERT INTO FinancialApp.Sales (id, amount, currency, customerId, employeeId, itemId, date)
                                         VALUES (2, '4500.00', 'EUR', 2000, 19, 24, '1/1/2008');

------Execution
    SELECT amount, currency, customerId, employeeId, itemId, date
      INTO actual
      FROM FinancialApp.Report('USD');

------Assertion
    CREATE TABLE expected (
	    amount MONEY,
	    currency CHAR(3),
	    customerId INT,
	    employeeId INT,
	    itemId INT,
	    date DATETIME
    );

	INSERT INTO expected (amount, currency, customerId, employeeId, itemId, date) SELECT 1260.00, 'USD', 1000, 7, 34, '2007-01-01';
	INSERT INTO expected (amount, currency, customerId, employeeId, itemId, date) SELECT 7200.00, 'USD', 2000, 19, 24, '2008-01-01';

	EXEC tSQLt.AssertEqualsTable 'expected', 'actual';
END;
GO

Here we’ve created a table that contains the actual results of the report and a second table containing the expected results. The AssertEqualsTable procedure will compare these two tables. If their schema or data is different, the test will fail with a report of the differences.

Example 4: SpyProcedure

Large monolithic stored procedures are difficult to test and maintain. We want to keep our stored procedures small and focused. We also want to test our stored procedures independantly of one another. To create independent tests, we can replace the functionality of a stored procedure with a spy. The spy will record the parameters that were passed to it.

In this example, we have improved our sales report to show either current or historical data based on a parameter. Here we’ll want to test that the SalesReport procedure handles the parameter correctly and calls either HistoricalReport or CurrentReport. We’ll use the AssertEqualsTable to make sure the currency parameter is passed correctly to HistoricalReport by looking in the spy’s log.

CREATE PROCEDURE testFinancialApp.[test that SalesReport calls HistoricalReport instead of CurrentReport when @showHistory = 1]
AS
BEGIN
-------Assemble
    EXEC tSQLt.SpyProcedure 'FinancialApp.HistoricalReport';
    EXEC tSQLt.SpyProcedure 'FinancialApp.CurrentReport';

-------Act
    EXEC FinancialApp.SalesReport 'USD', @showHistory = 1;

    SELECT currency
      INTO actual
      FROM FinancialApp.HistoricalReport_SpyProcedureLog;

-------Assert HistoricalReport got called with right parameter
    SELECT currency
      INTO expected
      FROM (SELECT 'USD') ex(currency);

    EXEC tSQLt.AssertEqualsTable 'expected', 'actual';
    
-------Assert CurrentReport did not get called
    IF EXISTS (SELECT 1 FROM FinancialApp.CurrentReport_SpyProcedureLog)
       EXEC tSQLt.Fail 'SalesReport should not have called CurrentReport when @showHistory = 1';
END;
GO

Example 5: ApplyConstraint

Testing database constraints has been a difficult problem for a long time. If we have a table and want to test a single constraint, we need to insert data that satisfies all the constraints on the table. This means that as we add new constraints to a table in the future, existing constraint tests are likely to start failing.

tSQLt allows for constraints to be isolated by first faking the table (which recreates the table without the constraints) and then applying the desired constraints to the fake table. The following test case shows how the validCurrency constraint can be tested.

CREATE PROCEDURE testFinancialApp.[test that Sales table does not allow invalid currency]
AS
BEGIN
    DECLARE @errorThrown bit; SET @errorThrown = 0;

    EXEC tSQLt.FakeTable 'FinancialApp', 'Sales';
    EXEC tSQLt.ApplyConstraint 'FinancialApp', 'Sales', 'validCurrency';

    BEGIN TRY
        INSERT INTO FinancialApp.Sales (id, currency)
                                VALUES (1, 'XYZ');
    END TRY
    BEGIN CATCH
        SET @errorThrown = 1;
    END CATCH;    

    IF (@errorThrown = 0 OR (EXISTS (SELECT 1 FROM FinancialApp.Sales)))
    BEGIN
        EXEC tSQLt.Fail 'Sales table should not allow invalid currency';
    END;

    IF EXISTS (SELECT 1 FROM FinancialApp.Sales)
        EXEC tSQLt.Fail 'Sales table should not allow invalid currency';
END;
GO

Wrap-Up
This tutorial has provided the basics of tSQLt test case writing. Test driven database development is your gateway to more robust, higher quality databases. Databases supported by automated tests are easier to refactor, maintain and tune for performance.


TEST CREATION AND EXECUTION

This section describes how test cases are created and executed, and provides the reference pages for the various methods to manage and execute test cases.

We’re going to answer two important questions in this section, “What happens when you create a test class?” and “What happens when a test class gets executed?”

What happens when you create a test class?
Not much, actually. When you create a test class using tSQLt.NewTestClass, a schema is created. That schema is created with an extended property so that tSQLt can later figure out which schemas are test classes. Note: If there is already a schema with the same name as the one you are trying to create, it is dropped first.

What happens when a test class gets executed?
If you execute tSQLt.RunTestClass, tSQLt does the following things:

1. It looks at all the stored procedures in the test class (schema) that start with the word “test”. These are all considered to be all the test cases for that test class.

2. For each of the test cases:
a. A record is created indicating that the test case is being executed in the tSQLt.TestResult table.
b. tSQLt starts a transaction.
c. If there is a stored procedure named SetUp on the test class, it is executed.
d. The test case stored procedure is executed.
e. The transaction is rolled-back.
f. The record in tSQLt.TestResult is updated accordingly if the test case succeeded, failed or threw an error.

3. The test results are displayed in the console.

If you execute tSQLt.RunAll, tSQLt first looks at all the schemas in the database for ones marked as test classes. Then, it follows steps 1 and 2 above for each test class. The test results are displayed after running all test classes.

Summary
Create test classes using tSQLt.NewTestClass. Execute test classes using tSQLt.RunTestClass or tSQLt.RunAll. When individual test cases are executed, they are wrapped in a transaction which is rolled-back. Before each test case, SetUp is called if it exists on the test class.



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱知菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值