演练:创建和运行数据库单元测试

在本演练中,您将创建一个验证若干存储过程的行为的数据库单元测试。 创建数据库单元测试可帮助识别可能导致错误应用程序行为的代码缺陷。 可以将数据库单元测试和应用程序测试作为一组自动执行的测试的一部分来运行。

在本演练中,您将执行下列任务:

  • 创建包含数据库架构的脚本

  • 创建数据库项目并导入该架构

  • 将数据库项目部署到独立开发环境中

  • 创建数据库单元测试

  • 定义测试逻辑

  • 运行数据库单元测试

  • 添加否定性单元测试

单元测试之一在存储过程中检测到错误后,您需要更正该错误并重新运行测试。

系统必备

若要完成本演练,必须能够连接到您在其上有权创建和部署数据库的数据库服务器。 有关更多信息,请参见执行 Visual Studio 的数据库功能所需的权限

创建包含数据库架构的脚本

创建可从中导入架构的脚本

  1. 在**“文件”菜单上指向“新建”,然后单击“文件”**。

    此时出现**“新建文件”**对话框。

  2. 在**“类别”列表中,如果尚未突出显示“常规”**,请单击它。

  3. 在**“模板”列表中,单击“Sql 文件”,然后单击“打开”**。

    Transact-SQL 编辑器打开。

  4. 复制下面的 Transact-SQL 代码并将其粘贴到 Transact-SQL 编辑器中。

    PRINT N'Creating Sales...';
    GO
    CREATE SCHEMA [Sales]
        AUTHORIZATION [dbo];
    GO
    PRINT N'Creating Sales.Customer...';
    GO
    CREATE TABLE [Sales].[Customer] (
        [CustomerID]   INT           IDENTITY (1, 1) NOT NULL,
        [CustomerName] NVARCHAR (40) NOT NULL,
        [YTDOrders]    INT           NOT NULL,
        [YTDSales]     INT           NOT NULL
    );
    GO
    PRINT N'Creating Sales.Orders...';
    GO
    CREATE TABLE [Sales].[Orders] (
        [CustomerID] INT      NOT NULL,
        [OrderID]    INT      IDENTITY (1, 1) NOT NULL,
        [OrderDate]  DATETIME NOT NULL,
        [FilledDate] DATETIME NULL,
        [Status]     CHAR (1) NOT NULL,
        [Amount]     INT      NOT NULL
    );
    GO
    PRINT N'Creating Sales.Def_Customer_YTDOrders...';
    GO
    ALTER TABLE [Sales].[Customer]
        ADD CONSTRAINT [Def_Customer_YTDOrders] DEFAULT 0 FOR [YTDOrders];
    GO
    PRINT N'Creating Sales.Def_Customer_YTDSales...';
    GO
    ALTER TABLE [Sales].[Customer]
        ADD CONSTRAINT [Def_Customer_YTDSales] DEFAULT 0 FOR [YTDSales];
    GO
    PRINT N'Creating Sales.Def_Orders_OrderDate...';
    GO
    ALTER TABLE [Sales].[Orders]
        ADD CONSTRAINT [Def_Orders_OrderDate] DEFAULT GetDate() FOR [OrderDate];
    GO
    PRINT N'Creating Sales.Def_Orders_Status...';
    GO
    ALTER TABLE [Sales].[Orders]
        ADD CONSTRAINT [Def_Orders_Status] DEFAULT 'O' FOR [Status];
    GO
    PRINT N'Creating Sales.PK_Customer_CustID...';
    GO
    ALTER TABLE [Sales].[Customer]
        ADD CONSTRAINT [PK_Customer_CustID] PRIMARY KEY CLUSTERED ([CustomerID] ASC) WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF);
    GO
    PRINT N'Creating Sales.PK_Orders_OrderID...';
    GO
    ALTER TABLE [Sales].[Orders]
        ADD CONSTRAINT [PK_Orders_OrderID] PRIMARY KEY CLUSTERED ([OrderID] ASC) WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF);
    GO
    PRINT N'Creating Sales.FK_Orders_Customer_CustID...';
    GO
    ALTER TABLE [Sales].[Orders]
        ADD CONSTRAINT [FK_Orders_Customer_CustID] FOREIGN KEY ([CustomerID]) REFERENCES [Sales].[Customer] ([CustomerID]) ON DELETE NO ACTION ON UPDATE NO ACTION;
    GO
    PRINT N'Creating Sales.CK_Orders_FilledDate...';
    GO
    ALTER TABLE [Sales].[Orders]
        ADD CONSTRAINT [CK_Orders_FilledDate] CHECK ((FilledDate >= OrderDate) AND (FilledDate < '01/01/2020'));
    GO
    PRINT N'Creating Sales.CK_Orders_OrderDate...';
    GO
    ALTER TABLE [Sales].[Orders]
        ADD CONSTRAINT [CK_Orders_OrderDate] CHECK ((OrderDate > '01/01/2005') and (OrderDate < '01/01/2020'));
    GO
    PRINT N'Creating Sales.uspCancelOrder...';
    GO
    CREATE PROCEDURE [Sales].[uspCancelOrder]
    @OrderID INT
    AS
    BEGIN
    DECLARE @Delta INT, @CustomerID INT
    BEGIN TRANSACTION
        SELECT @Delta = [Amount], @CustomerID = [CustomerID]
         FROM [Sales].[Orders] WHERE [OrderID] = @OrderID;
    
    UPDATE [Sales].[Orders]
       SET [Status] = 'X'
    WHERE [OrderID] = @OrderID;
    
    UPDATE [Sales].[Customer]
       SET
       YTDOrders = YTDOrders - @Delta
        WHERE [CustomerID] = @CustomerID
    COMMIT TRANSACTION
    END
    GO
    PRINT N'Creating Sales.uspFillOrder...';
    GO
    CREATE PROCEDURE [Sales].[uspFillOrder]
    @OrderID INT, @FilledDate DATETIME
    AS
    BEGIN
    DECLARE @Delta INT, @CustomerID INT
    BEGIN TRANSACTION
        SELECT @Delta = [Amount], @CustomerID = [CustomerID]
         FROM [Sales].[Orders] WHERE [OrderID] = @OrderID;
    
    UPDATE [Sales].[Orders]
       SET [Status] = 'F',
           [FilledDate] = @FilledDate
    WHERE [OrderID] = @OrderID;
    
    UPDATE [Sales].[Customer]
       SET
       YTDSales = YTDSales - @Delta
        WHERE [CustomerID] = @CustomerID
    COMMIT TRANSACTION
    END
    GO
    PRINT N'Creating Sales.uspNewCustomer...';
    GO
    CREATE PROCEDURE [Sales].[uspNewCustomer]
    @CustomerName NVARCHAR (40)
    AS
    BEGIN
    INSERT INTO [Sales].[Customer] (CustomerName) VALUES (@CustomerName);
    SELECT SCOPE_IDENTITY()
    END
    GO
    PRINT N'Creating Sales.uspPlaceNewOrder...';
    GO
    CREATE PROCEDURE [Sales].[uspPlaceNewOrder]
    @CustomerID INT, @Amount INT, @OrderDate DATETIME, @Status CHAR (1)='O'
    AS
    BEGIN
    DECLARE @RC INT
    BEGIN TRANSACTION
    INSERT INTO [Sales].[Orders] (CustomerID, OrderDate, FilledDate, Status, Amount) 
         VALUES (@CustomerID, @OrderDate, NULL, @Status, @Amount)
    SELECT @RC = SCOPE_IDENTITY();
    UPDATE [Sales].[Customer]
       SET
       YTDOrders = YTDOrders + @Amount
        WHERE [CustomerID] = @CustomerID
    COMMIT TRANSACTION
    RETURN @RC
    END
    GO
    CREATE PROCEDURE [Sales].[uspShowOrderDetails]
    @CustomerID INT=0
    AS
    BEGIN
    SELECT [C].[CustomerName], CONVERT(date, [O].[OrderDate]), CONVERT(date, [O].[FilledDate]), [O].[Status], [O].[Amount]
      FROM [Sales].[Customer] AS C
      INNER JOIN [Sales].[Orders] AS O
         ON [O].[CustomerID] = [C].[CustomerID]
      WHERE [C].[CustomerID] = @CustomerID
    END
    GO
    
  5. 在**“文件”菜单上,单击“将 SqlQuery_1.sql 另存为”**。

    将出现**“另存文件为”**对话框。

  6. 在**“对象名”**中,键入 SampleImportScript.sql。

    可以将文件保存到计算机中的任何位置。 记下该位置,因为在下面的步骤中必须使用此脚本。

  7. 单击**“保存”**。

  8. 在**“文件”菜单上,单击“关闭解决方案”**。

    接下来,创建一个数据库项目,并从已创建的脚本导入架构。

创建数据库项目并导入架构

创建数据库项目

  1. 在**“文件”菜单上指向“新建”,再单击“项目”**。

    此时将出现**“新建项目”**对话框。

  2. 在**“已安装的模板”下,展开“数据库”节点,然后单击“SQL Server”**。

    提示

    如果您使用的是 Visual Studio 专业版,则请在“已安装的模板”下查看,展开“数据库”节点,展开“SQL Server”节点,然后单击“高级”

  3. 在模板列表中单击**“SQL Server 2008 数据库项目”**。

    提示

    如果您计划将数据库部署到不同的数据库版本,则请选择与目标服务器对应的模板。

  4. 在**“名称”**中,键入“SimpleUnitTestDB”。

  5. 如果**“创建解决方案的目录”**复选框尚未选中,则选中该复选框。

  6. 如果**“添加到源代码管理”复选框尚未清除,则清除该复选框,并单击“确定”**。

    数据库项目会创建并出现在**“解决方案资源管理器”**中。 接下来,从脚本中导入数据库架构。

从脚本中导入数据库

  1. 在**“项目”菜单上,单击“导入脚本”**。

  2. 阅读“欢迎”页之后,单击**“下一步”**。

  3. 单击**“浏览”**,指出保存 SampleImportScript.sql 文件的路径。

  4. 双击 SampleImportScript.sql 文件,然后单击**“完成”**。

    将导入脚本,在该脚本中定义的对象添加到数据库项目中。

  5. 查看摘要,然后单击**“完成”**完成操作。

    提示

    Sales.uspFillOrder 过程包含一个故意生成的代码错误,您将发现该错误并在此过程稍后的步骤中更正它。

检查产生的项目

  1. 在**“解决方案资源管理器”中,展开“架构对象”**子节点。

  2. 浏览层次结构中**“架构对象”**节点下的子节点。

    **“解决方案资源管理器”**包含定义数据库对象的文件。

  3. 从**“视图”菜单,单击“数据库架构视图”**。

  4. 在**“架构视图”**中展开“SimpleUnitTestDB”节点。

  5. 浏览层次结构中“SimpleUnitTestDB”节点下的子节点。

    **“架构视图”包含“解决方案资源管理器”**中显示的文件中定义的对象。

部署到独立开发环境

接下来,部署项目以创建一个具有导入架构但没有数据的数据库。 在独立开发环境(或沙箱)中创建此数据库,以便可以在没有外界干扰的情况下开发和测试此数据库。

配置和生成数据库项目

  1. 在**“解决方案资源管理器”**中,单击数据库项目“SimpleUnitTestDB”。

  2. 在**“项目”菜单上,单击“SimpleUnitTestDB 属性”**。

    即会显示该项目的属性对话框。

  3. 单击**“部署”**选项卡。

  4. 在**“配置部署设置”列表中,单击“我的独立开发环境”**。 通过为独立开发环境配置设置,可以使用与在其他环境(如临时服务器和生产服务器)中不同的部署设置。

  5. 在**“部署操作”列表中,单击“创建部署脚本(.sql)并部署到数据库”**。

  6. 在**“目标数据库设置”中,单击“编辑”**。

    随即出现**“连接属性”**对话框。

  7. 设置要创建的数据库的连接属性,然后单击**“确定”**。

    在**“目标连接”**框中,出现正确的连接字符串。

    警告

    应在测试服务器、开发服务器或本地计算机上创建数据库。 不应指定生产服务器。

  8. 在**“目标数据库名称”**中,键入“SimpleUnitTestDB”。

  9. 在**“部署配置文件”旁,单击“编辑”**。

  10. 清除**“如果可能发生数据丢失则阻止增量部署”**复选框。

    提示

    对于本演练,您将针对在数据库单元测试过程中部署的空数据库来测试存储过程。 由于将在独立开发环境中测试存储过程,因此不必保留任何现有数据。

  11. 在**“文件”菜单上,单击“全部保存”**。

  12. 在**“生成”菜单上,单击“生成解决方案”**。

    刚刚设置的属性将确定生成部署脚本的方式。 生成状态显示在**“输出”窗口中,最后一行应显示“生成: 1 成功或最新”**。

    提示

    如果未显示“输出”窗口,请打开“视图”菜单,然后单击“输出”

部署数据库项目

  1. 在**“解决方案资源管理器”**中,单击“SimpleUnitTestDB”数据库项目。

  2. 在**“生成”菜单上,单击“部署 SimpleUnitTestDB”**。

    警告

    应对测试服务器、开发服务器或本地计算机运行此部署。 不应指定生产服务器。

    数据库项目部署到新数据库。 部署状态显示在**“输出”窗口中,最后一行应显示“部署: 成功 1 个”**。 可能要定义数据生成计划以在数据库中创建测试数据。 对于本演练,将测试一个非常简单的数据库,您不需要为该数据库生成数据。

创建数据库单元测试

创建针对存储过程的数据库单元测试

  1. 在**“视图”菜单上,单击“数据库架构视图”**。

  2. 在**“架构视图”中,依次展开“架构”节点、“销售”节点、“可编程性”节点,然后展开“存储过程”**节点。

  3. 右击**“uspNewCustomer”存储过程,然后单击“创建单元测试”**。

    随即出现**“创建单元测试”**对话框。

  4. 选中针对存储过程的全部五个复选框:“Sales.uspCancelOrder”“Sales.uspFillOrder”“Sales.uspNewCustomer”“Sales.uspPlaceNewOrder”“Sales.uspShowOrderDetails”

  5. 在**“项目”中,单击“创建新的 Visual C# 测试项目**。

  6. 接受项目名称和类名称的默认名称,然后单击**“确定”**。

    将出现**“项目‘TestProject1’配置”**对话框。

  7. 在**“使用以下数据连接执行单元测试:”**中,指定与在本演练前面的步骤中部署的数据库的连接。

    提示

    如果必须测试具有有限权限的视图或存储过程,通常在此步骤中指定该连接。 然后,指定具有更广泛权限的辅助连接,以验证测试。 如果您有辅助连接,则应当将该用户添加到数据库项目,在预先部署脚本中创建该用户的登录名。

  8. 在**“部署”中,选中“在单元测试运行前自动部署该数据库项目”**复选框。

  9. 在**“数据库项目”中单击“SimpleUnitTestDB.dbproj”**。

  10. 在**“部署配置”中,单击“调试”**。

    在数据库单元测试过程中,可能还需要生成测试数据。 对于本演练,将跳过该步骤,因为测试会创建其自己的数据。

  11. 单击**“确定”**。

    将生成测试项目并显示数据库单元测试设计器。 接下来,将在单元测试的 Transact-SQL 脚本中更新测试逻辑。

定义测试逻辑

此非常简单的数据库具有两个表:Customer 和 Order。 使用以下存储过程更新数据库:

  • uspNewCustomer - 此存储过程向 Customer 表中添加记录,该表将客户的 YTDOrders 和 YTDSales 列设置为零。

  • uspPlaceNewOrder - 此存储过程为指定客户向 Orders 表中添加记录,并更新 Customer 表中对应记录的 YTDOrders 值。

  • uspFillOrder - 此存储过程通过将状态从“O”更改为“F”来更新 Orders 表中的记录,并增大 Customer 表中对应记录的 YTDSales 数额。

  • uspCancelOrder - 此存储过程通过将状态从“O”更改为“X”来更新 Orders 表中的记录,并减小 Customer 表中对应记录的 YTDOrders 数额。

  • uspShowOrderDetails - 此存储过程将 Orders 表与 Custom 表进行联接,并显示特定客户的记录。

提示

此示例演示如何创建简单数据库单元测试。 在真实数据库中,可以计算特定客户的所有状态为“O”或“F”的订单的总额。 本演练中的过程不包含错误处理。 例如,不阻止您针对已填充的订单调用 uspFillOrder。

测试假定数据库在干净的状态下启动。 您将创建验证以下条件的测试:

  • uspNewCustomer - 验证 Customer 表是否在您运行存储过程后包含一行。

  • uspPlaceNewOrder - 对于 CustomerID 为 1 的客户,生成一个金额为 $100 的订单。 验证该客户的 YTDOrders 金额是否为 100,YTDSales 金额是否为零。

  • uspFillOrder - 对于 CustomerID 为 1 的客户,生成一个金额为 $50 的订单。 填充该订单。 验证 YTDOrders 和 YTDSales 金额是否都为 50。

  • uspShowOrderDetails - 对于 CustomerID 为 1 的客户,生成金额为 $100、$50 和 $5 的订单。 验证 uspShowOrderDetails 是否返回正确的列数,以及结果集是否具有期望的校验和。

提示

为了获得完整的数据库单元测试集,通常要验证是否正确设置了其他列。 为了将本演练的大小控制在可管理的范围,本演练不描述如何验证 uspCancelOrder 的行为。

编写针对 uspNewCustomer 的数据库单元测试

  1. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspNewCustomerTest”,并确保在相邻的列表中突出显示“测试”**。

    执行上一步骤之后,可以为在单元测试中的测试操作创建测试脚本。

  2. 更新 Transact-SQL 编辑器中的 Transact-SQL 语句,使之与以下语句相符:

    -- database unit test for Sales.uspNewCustomer
    DECLARE @RC AS INT, @CustomerName AS NVARCHAR (40);
    
    SELECT @RC = 0,
           @CustomerName = 'Fictitious Customer';
    
    EXECUTE @RC = [Sales].[uspNewCustomer] @CustomerName;
    
    SELECT * FROM [Sales].[Customer];
    
  3. 在**“测试条件”窗格中,单击“没有结论”测试条件,然后单击“删除测试条件”**(x)。

  4. 在**“测试条件”窗格中,单击列表中的“行数”,然后单击“添加测试条件”**(+)。

  5. 在**“属性”窗口中,将“行数”**属性设置为 1。

  6. 在**“文件”菜单上,单击“全部保存”**。

    接下来,将定义针对 uspPlaceNewOrder 的单元测试逻辑。

编写针对 uspPlaceNewOrder 的数据库单元测试

  1. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspPlaceNewOrderTest”,并确保在相邻的列表中突出显示“测试”**。

    执行此步骤之后,可以创建测试脚本用于在单元测试中执行测试操作。

  2. 更新 Transact-SQL 编辑器中的 Transact-SQL 语句,使之与以下语句相符:

    -- database unit test for Sales.uspPlaceNewOrder
    DECLARE @RC AS INT, @CustomerID AS INT, @Amount AS INT, @OrderDate AS DATETIME, @Status AS CHAR (1);
    DECLARE @CustomerName AS NVARCHAR(40);
    
    SELECT @RC = 0,
           @CustomerID = 0,
           @CustomerName = N'Fictitious Customer',
           @Amount = 100,
           @OrderDate = getdate(),
           @Status = 'O';
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    
    -- place an order for that customer
    EXECUTE @RC = [Sales].[uspPlaceNewOrder] @CustomerID, @Amount, @OrderDate, @Status;
    
    -- verify that the YTDOrders value is correct.
    SELECT @RC = [YTDOrders] FROM [Sales].[Customer] WHERE [CustomerID] = @CustomerID
    
    SELECT @RC AS RC
    
  3. 在**“测试条件”窗格中,单击“没有结论”测试条件,然后单击“删除测试条件”**(x)。

  4. 在**“测试条件”窗格中,单击列表中的“标量值”,然后单击“添加测试条件”**(+)。

  5. 在**“属性”窗口中,将“所需的值”**属性设置为 100。

  6. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspPlaceNewOrderTest”,并确保在相邻的列表中突出显示“预先测试”**。

    执行此步骤之后,可以指定一些语句,用于将数据置于执行测试所需的状态。 对于此示例,在下订单之前必须先创建客户记录。

  7. 单击**“单击此处以创建”**创建预先测试脚本。

  8. 更新 Transact-SQL 编辑器中的 Transact-SQL 语句,使之与以下语句相符:

    /*
    Add Transact-SQL statements here that you want to run before
    the test script is run.
    */
    -- Add a customer for this test with the name 'Fictitious Customer'
    DECLARE @NewCustomerID AS INT, @CustomerID AS INT, @RC AS INT, @CustomerName AS NVARCHAR (40);
    
    SELECT @RC = 0,
           @NewCustomerID = 0,
       @CustomerID = 0,
           @CustomerName = N'Fictitious Customer';
    
    IF NOT EXISTS(SELECT * FROM [Sales].[Customer] WHERE CustomerName = @CustomerName)
    BEGIN
    EXECUTE @NewCustomerID = [Sales].[uspNewCustomer] @CustomerName;
    END
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    
    -- delete any old records in the Orders table and clear out the YTD Sales/Orders fields
    DELETE from [Sales].[Orders] WHERE [CustomerID] = @CustomerID;
    UPDATE [Sales].[Customer] SET YTDOrders = 0, YTDSales = 0 WHERE [CustomerID] = @CustomerID;
    
  9. 在**“文件”菜单上,单击“全部保存”**。

    接下来,将创建针对 uspFillOrder 的单元测试。

编写针对 uspFillOrder 的数据库单元测试

  1. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspFillOrderTest”,并确保在相邻的列表中突出显示“测试”**。

    执行此步骤之后,可以创建测试脚本用于在单元测试中执行测试操作。

  2. 更新 Transact-SQL 编辑器中的 Transact-SQL 语句,使之与以下语句相符:

    -- database unit test for Sales.uspFillOrder
    DECLARE @RC AS INT, @CustomerID AS INT, @Amount AS INT, @FilledDate AS DATETIME, @Status AS CHAR (1);
    DECLARE @CustomerName AS NVARCHAR(40), @OrderID AS INT;
    
    SELECT @RC = 0,
           @CustomerID = 0,
           @OrderID = 0,
           @CustomerName = N'Fictitious Customer',
           @Amount = 100,
           @FilledDate = getdate(),
           @Status = 'O';
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    -- Get the most recently added order.
    SELECT @OrderID = MAX([OrderID]) FROM [Sales].[Orders] WHERE [CustomerID] = @CustomerID;
    
    -- fill an order for that customer
    EXECUTE @RC = [Sales].[uspFillOrder] @OrderID, @FilledDate;
    
    -- verify that the YTDOrders value is correct.
    SELECT @RC = [YTDSales] FROM [Sales].[Customer] WHERE [CustomerID] = @CustomerID
    
    SELECT @RC AS RC;
    
  3. 在**“测试条件”窗格中,单击“没有结论”测试条件,然后单击“删除测试条件”**(x)。

  4. 在**“测试条件”窗格中,单击列表中的“标量值”,然后单击“添加测试条件”**(+)。

  5. 在**“属性”窗口中,将“所需的值”**属性设置为 100。

  6. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspFillOrderTest”,并确保在相邻的列表中突出显示“预先测试”**。 执行此步骤之后,可以指定一些语句,用于将数据置于执行测试所需的状态。 对于此示例,在下订单之前必须先创建客户记录。

  7. 单击**“单击此处以创建”**创建预先测试脚本。

  8. 更新 Transact-SQL 编辑器中的 Transact-SQL 语句,使之与以下语句相符:

    /*
    Add Transact-SQL statements here that you want to run before
    the test script is run.
    */
    BEGIN TRANSACTION
    
    -- Add a customer for this test with the name 'CustomerB'
    DECLARE @NewCustomerID AS INT, @RC AS INT, @CustomerName AS NVARCHAR (40);
    
    SELECT @RC = 0,
           @NewCustomerID = 0,
           @CustomerName = N'Fictitious Customer';
    
    IF NOT EXISTS(SELECT * FROM [Sales].[Customer] WHERE CustomerName = @CustomerName)
    BEGIN
    EXECUTE @NewCustomerID = [Sales].[uspNewCustomer] @CustomerName;
    END
    
    DECLARE @CustomerID AS INT, @Amount AS INT, @OrderDate AS DATETIME, @Status AS CHAR (1);
    
    SELECT @RC = 0,
           @CustomerID = 0,
           @CustomerName = N'Fictitious Customer',
           @Amount = 100,
           @OrderDate = getdate(),
           @Status = 'O';
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    
    -- delete any old records in the Orders table and clear out the YTD Sales/Orders fields
    DELETE from [Sales].[Orders] WHERE [CustomerID] = @CustomerID;
    UPDATE [Sales].[Customer] SET YTDOrders = 0, YTDSales = 0 WHERE [CustomerID] = @CustomerID;
    
    -- place an order for that customer
    EXECUTE @RC = [Sales].[uspPlaceNewOrder] @CustomerID, @Amount, @OrderDate, @Status;
    
    COMMIT TRANSACTION
    
  9. 在**“文件”菜单上,单击“全部保存”**。

    此时,已准备好运行测试了。

编写针对 uspShowOrderDetails 的数据库单元测试

  1. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspShowOrderDetailsTest”,并确保在相邻的列表中突出显示“测试”**。

    执行此步骤之后,可以创建测试脚本用于在单元测试中执行测试操作。

  2. 更新 Transact-SQL 编辑器中的 Transact-SQL 语句,使之与以下语句相符:

    -- database unit test for Sales.uspFillOrder
    DECLARE @RC AS INT, @CustomerID AS INT, @Amount AS INT, @FilledDate AS DATETIME, @Status AS CHAR (1);
    DECLARE @CustomerName AS NVARCHAR(40), @OrderID AS INT;
    
    SELECT @RC = 0,
           @CustomerID = 0,
           @OrderID = 0,
           @CustomerName = N'Fictitious Customer',
           @Amount = 100,
           @FilledDate = getdate(),
           @Status = 'O';
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    
    -- fill an order for that customer
    EXECUTE @RC = [Sales].[uspShowOrderDetails] @CustomerID;
    
    SELECT @RC AS RC;
    
  3. 在**“测试条件”窗格中,单击“没有结论”测试条件,然后单击“删除测试条件”**(x)。

  4. 在**“测试条件”窗格中,单击列表中的“所需的架构”,然后单击“添加测试条件”**(+)。

  5. 在**“属性”窗口中,在“配置”属性中,单击浏览按钮(“…”**)。

  6. 在**“expectedSchemaCondition1 的配置”**对话框中,指定与数据库的连接。

  7. 单击**“检索”**。

    将执行单元测试 Transact-SQL 的主体,生成的架构出现在对话框中。 由于未执行预先测试代码,因此不返回任何数据。 因为您只验证架构,不验证数据,因此不返回数据是允许的。

  8. 单击**“确定”**。

    所需的架构随测试条件一起存储。

  9. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspShowOrderDetailsTest”,并确保在相邻的列表中突出显示“预先测试”**。 执行此步骤之后,可以指定一些语句,用于将数据置于执行测试所需的状态。 对于此示例,在下订单之前必须先创建客户记录。

  10. 单击**“单击此处以创建”**创建预先测试脚本。

  11. 更新 Transact-SQL 编辑器中的 Transact-SQL 语句,使之与以下语句相符:

    /*
    Add Transact-SQL statements here that you want to run before
    the test script is run.
    */
    BEGIN TRANSACTION
    
    -- Add a customer for this test with the name 'FictitiousCustomer'
    DECLARE @NewCustomerID AS INT, @RC AS INT, @CustomerName AS NVARCHAR (40);
    
    SELECT @RC = 0,
           @NewCustomerID = 0,
           @CustomerName = N'Fictitious Customer';
    
    IF NOT EXISTS(SELECT * FROM [Sales].[Customer] WHERE CustomerName = @CustomerName)
    BEGIN
    EXECUTE @NewCustomerID = [Sales].[uspNewCustomer] @CustomerName;
    END
    
    
    DECLARE @CustomerID AS INT, @Amount AS INT, @OrderDate AS DATETIME, @Status AS CHAR (1);
    
    SELECT @RC = 0,
           @CustomerID = 0,
           @CustomerName = N'Fictitious Customer',
           @OrderDate = getdate(),
           @Status = 'O';
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    
    -- delete any old records in the Orders table and clear out the YTD Sales/Orders fields
    DELETE from [Sales].[Orders] WHERE [CustomerID] = @CustomerID;
    UPDATE [Sales].[Customer] SET YTDOrders = 0, YTDSales = 0 WHERE [CustomerID] = @CustomerID;
    
    -- place 3 orders for that customer
    EXECUTE @RC = [Sales].[uspPlaceNewOrder] @CustomerID, 100, @OrderDate, @Status;
    EXECUTE @RC = [Sales].[uspPlaceNewOrder] @CustomerID, 50, @OrderDate, @Status;
    EXECUTE @RC = [Sales].[uspPlaceNewOrder] @CustomerID, 5, @OrderDate, @Status;
    
    COMMIT TRANSACTION
    
  12. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspShowOrderDetailsTest”,然后在相邻的列表中单击“测试”**。

    必须执行此操作,因为您要将校验和条件应用于测试,而不是应用于预先测试。

  13. 在**“测试条件”窗格中,单击列表中的“数据校验和”,然后单击“添加测试条件”**(+)。

  14. 在**“属性”窗口中,在“配置”属性中,单击浏览按钮(“…”**)。

  15. 在**“checksumCondition1 的配置”**对话框中,指定与数据库的连接。

  16. 将对话框中的 Transact-SQL 替换为以下代码:

    BEGIN TRANSACTION
    
    -- Add a customer for this test with the name 'CustomerB'
    DECLARE @NewCustomerID AS INT, @RC AS INT, @CustomerName AS NVARCHAR (40);
    
    SELECT @RC = 0,
           @NewCustomerID = 0,
           @CustomerName = N'Fictitious Customer';
    
    IF NOT EXISTS(SELECT * FROM [Sales].[Customer] WHERE CustomerName = @CustomerName)
    BEGIN
    EXECUTE @NewCustomerID = [Sales].[uspNewCustomer] @CustomerName;
    END
    
    
    DECLARE @CustomerID AS INT, @Amount AS INT, @OrderDate AS DATETIME, @Status AS CHAR (1);
    
    SELECT @RC = 0,
           @CustomerID = 0,
           @CustomerName = N'Fictitious Customer',
           @OrderDate = getdate(),
           @Status = 'O';
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    
    -- delete any old records in the Orders table and clear out the YTD Sales/Orders fields
    DELETE from [Sales].[Orders] WHERE [CustomerID] = @CustomerID;
    UPDATE [Sales].[Customer] SET YTDOrders = 0, YTDSales = 0 WHERE [CustomerID] = @CustomerID;
    
    
    
    -- place 3 orders for that customer
    EXECUTE @RC = [Sales].[uspPlaceNewOrder] @CustomerID, 100, @OrderDate, @Status;
    EXECUTE @RC = [Sales].[uspPlaceNewOrder] @CustomerID, 50, @OrderDate, @Status;
    EXECUTE @RC = [Sales].[uspPlaceNewOrder] @CustomerID, 5, @OrderDate, @Status;
    
    
    COMMIT TRANSACTION
    
    
    -- database unit test for Sales.uspFillOrder
    DECLARE @FilledDate AS DATETIME;
    DECLARE @OrderID AS INT;
    
    SELECT @RC = 0,
           @CustomerID = 0,
           @OrderID = 0,
           @CustomerName = N'Fictitious Customer',
           @Amount = 100,
           @FilledDate = getdate(),
           @Status = 'O';
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    
    -- fill an order for that customer
    EXECUTE @RC = [Sales].[uspShowOrderDetails] @CustomerID;
    
    SELECT @RC AS RC;
    

    此代码将预先测试中的 Transact-SQL 代码与测试自身中的 Transact-SQL 代码合并在一起。 您同时需要这两种代码,以便返回与运行测试时返回结果相同的结果。

  17. 单击**“检索”**。

    将执行指定的 Transact-SQL,并针对返回的数据计算校验和。

  18. 单击**“确定”**。

    计算出的校验和随测试条件一起存储。 所需的校验和出现在“数据校验和”测试条件的“值”列中。

  19. 在**“文件”菜单上,单击“全部保存”**。

    此时,已准备好运行测试了。

运行数据库单元测试

运行数据库单元测试

  1. 在**“测试”菜单上,指向“窗口”,然后单击“测试视图”**。

  2. 在“测试视图”窗口的工具栏中,单击**“刷新”**以更新测试列表。

    **“测试视图”**窗口列出在本演练的早期过程中创建的测试,以及向其中添加了 Transact-SQL 语句和测试条件的测试。 名为 TestMethod1 的测试是空的,在本演练中将不使用它。

  3. 右击**“Sales_uspNewCustomerTest”,然后单击“运行选定内容”**。

    Visual Studio 使用您指定的特权上下文连接到数据库并应用数据生成计划。 之后,Visual Studio 切换到执行上下文,然后在测试中运行 Transact-SQL 脚本。 最后,Visual Studio 会根据测试条件中指定的内容计算 Transact-SQL 脚本的结果,并在**“测试结果”**窗口中显示结果(通过或失败)。

  4. 在**“测试结果”**窗口中查看结果。

    测试通过意味着 SELECT 语句将在运行时返回一行。

  5. 对 Sales_uspPlaceNewOrderTest、Sales_uspFillOrderTest 和 Sales_uspShowOrderDetailsTest 测试重复步骤 3。 结果应如下所示:

    测试

    预期结果

    Sales_uspPlaceNewOrderTest

    通过

    Sales_uspShowOrderDetailsTest

    通过

    Sales_uspFillOrderTest

    测试失败,出现以下错误:“ScalarValueCondition 条件(scalarValueCondition2)失败: 结果集 1 行 1 列 1: 值不匹配,实际为‘-100’,应为‘100’”。出现此错误的原因是存储过程的定义包含小错误。

    接下来,您将更正该错误并重新运行测试。

更正 Sales.uspFillOrder 中的错误

  1. 在**“架构视图”中,双击“uspFillOrder”**存储过程,以便在 Transact-SQL 编辑器中打开其定义。

  2. 在定义中,找到以下 Transact-SQL 语句:

    UPDATE [Sales].[Customer]
       SET
       YTDSales = YTDSales - @Delta
        WHERE [CustomerID] = @CustomerID
    
  3. 更改语句中的 SET 子句,使之与以下语句相符:

    UPDATE [Sales].[Customer]
       SET
       YTDSales = YTDSales + @Delta
        WHERE [CustomerID] = @CustomerID
    
  4. 在**“文件”菜单上,单击“保存 uspFillOrder.proc.sql”**。

  5. 在**“测试视图”中,右击“Sales_uspFillOrderTest”,然后单击“运行选定内容”**。

    测试通过。

添加否定性单元测试

您可以创建否定性测试,以验证测试会在应当失败时失败。 例如,如果尝试取消已填充的订单,则测试应当失败。 在本演练部分中,您将针对 Sales.uspCancelOrder 存储过程创建一个否定性单元测试。

若要创建并验证否定性测试,必须执行以下任务:

  • 更新存储过程以测试失败条件

  • 定义新的单元测试

  • 修改单元测试的代码以指示期望失败

  • 运行单元测试

更新存储过程

  1. 在架构视图中,依次展开“SimpleUnitTestDB”节点、“架构”节点、“销售”节点、“可编程性”节点和“存储过程”节点,然后双击“uspCancelOrder”。

  2. 在 Transact-SQL 编辑器中,更新过程定义以匹配下面的代码:

    CREATE PROCEDURE [Sales].[uspCancelOrder]
    @OrderID INT
    AS
    BEGIN
        DECLARE @Delta INT, @CustomerID INT, @PriorStatus CHAR(1)
        BEGIN TRANSACTION
            BEGIN TRY
                IF (NOT EXISTS(SELECT [CustomerID] from [Sales].[Orders] WHERE [OrderID] = @OrderID))
                BEGIN
                    -- Specify WITH LOG option so that the error is
                    -- written to the application log.
                    RAISERROR( 'That order does not exist.', -- Message text
                               16, -- severity
                                1 -- state
                            ) WITH LOG;
                END
    
                SELECT @Delta = [Amount], @CustomerID = [CustomerID], @PriorStatus = [Status]
                 FROM [Sales].[Orders] WHERE [OrderID] = @OrderID
    
                IF @PriorStatus <> 'O' 
                BEGIN
                    -- Specify WITH LOG option so that the error is
                    -- written to the application log.
                    RAISERROR ( 'You can only cancel open orders.', -- Message text
                                16, -- Severity
                                1 -- State
                                ) WITH LOG;
                END
                ELSE
                BEGIN
                    -- If we make it to here, then we can cancel the order. Update the status to 'X' first...
                    UPDATE [Sales].[Orders]
                       SET [Status] = 'X'
                    WHERE [OrderID] = @OrderID
                    -- and then remove the amount from the YTDOrders for the customer
                    UPDATE [Sales].[Customer]
                           SET
                               YTDOrders = YTDOrders - @Delta
                    WHERE [CustomerID] = @CustomerID
                    COMMIT TRANSACTION
                    RETURN 1; -- indicate success
                END
            END TRY
            BEGIN CATCH
                DECLARE @ErrorMessage NVARCHAR(4000);
                DECLARE @ErrorSeverity INT;
                DECLARE @ErrorState INT;
    
                SELECT @ErrorMessage = ERROR_MESSAGE(),
                       @ErrorSeverity = ERROR_SEVERITY(),
                       @ErrorState = ERROR_STATE();
    
                ROLLBACK TRANSACTION
                -- Use RAISERROR inside the CATCH block to return
                -- error information about the original error that
                -- caused execution to jump to the CATCH block.
                RAISERROR (@ErrorMessage, -- Mesasge text
                           @ErrorSeverity, -- Severity
                           @ErrorState -- State
                          );
                RETURN 0; -- indicate failure
            END CATCH;
    END
    
  3. 在**“文件”菜单上,单击“保存 uspCancelOrder.proc.sql”**。

  4. 在**“解决方案资源管理器”中,右击“SimpleUnitTestDB”,然后单击“部署”**。

    您将更新部署到 uspCancelOrder 存储过程。 您未更改其他对象,因此仅更新了该存储过程。

    接下来,将为此过程定义关联的单元测试。

编写针对 uspCancelOrder 的数据库单元测试

  1. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspCancelOrderTest”,并确保在相邻的列表中突出显示“测试”**。

    执行此步骤之后,可以创建测试脚本用于在单元测试中执行测试操作。

  2. 更新 Transact-SQL 编辑器中的 Transact-SQL 语句,使之与以下语句相符:

    -- database unit test for Sales.uspFillOrder
    DECLARE @RC AS INT, @CustomerID AS INT, @Amount AS INT, @FilledDate AS DATETIME, @Status AS CHAR (1);
    DECLARE @CustomerName AS NVARCHAR(40), @OrderID AS INT;
    
    SELECT @RC = 0,
           @CustomerID = 0,
           @OrderID = 0,
           @CustomerName = N'Fictitious Customer',
           @Amount = 100,
           @FilledDate = getdate(),
           @Status = 'O';
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    -- Get the most recently added order.
    SELECT @OrderID = MAX([OrderID]) FROM [Sales].[Orders] WHERE [CustomerID] = @CustomerID;
    
    -- try to cancel an order for that customer that has already been filled
    EXECUTE @RC = [Sales].[uspCancelOrder] @OrderID;
    
    SELECT @RC AS RC;
    
  3. 在**“测试条件”窗格中,单击“没有结论”测试条件,然后单击“删除测试条件”**(x)。

  4. 在**“测试条件”窗格中,单击列表中的“标量值”,然后单击“添加测试条件”**(+)。

  5. 在**“属性”窗口中,将“所需的值”**属性设置为 0。

  6. 在数据库单元测试设计器的导航栏中,单击**“Sales_uspCancelOrderTest”,并确保在相邻的列表中突出显示“预先测试”**。 执行此步骤之后,可以指定一些语句,用于将数据置于执行测试所需的状态。 对于此示例,在下订单之前必须先创建客户记录。

  7. 单击**“单击此处以创建”**创建预先测试脚本。

  8. 更新 Transact-SQL 编辑器中的 Transact-SQL 语句,使之与以下语句相符:

    /*
    Add Transact-SQL statements here that you want to run before
    the test script is run.
    */
    BEGIN TRANSACTION
    
    -- Add a customer for this test with the name 'CustomerB'
    DECLARE @NewCustomerID AS INT, @RC AS INT, @CustomerName AS NVARCHAR (40);
    
    SELECT @RC = 0,
           @NewCustomerID = 0,
           @CustomerName = N'Fictitious Customer';
    
    IF NOT EXISTS(SELECT * FROM [Sales].[Customer] WHERE CustomerName = @CustomerName)
    BEGIN
    EXECUTE @NewCustomerID = [Sales].[uspNewCustomer] @CustomerName;
    END
    
    DECLARE @CustomerID AS INT, @Amount AS INT, @OrderDate AS DATETIME, @FilledDate AS DATETIME, @Status AS CHAR (1), @OrderID AS INT;
    
    SELECT @RC = 0,
           @CustomerID = 0,
       @OrderID = 0,
           @CustomerName = N'Fictitious Customer',
           @Amount = 100,
           @OrderDate = getdate(),
       @FilledDate = getdate(),
           @Status = 'O';
    
    -- NOTE: Assumes that you inserted a Customer record with CustomerName='Fictitious Customer' in the pre-test script.
    SELECT @CustomerID = [CustomerID] FROM [Sales].[Customer] WHERE [CustomerName] = @CustomerName;
    
    -- delete any old records in the Orders table and clear out the YTD Sales/Orders fields
    DELETE from [Sales].[Orders] WHERE [CustomerID] = @CustomerID;
    UPDATE [Sales].[Customer] SET YTDOrders = 0, YTDSales = 0 WHERE [CustomerID] = @CustomerID;
    
    -- place an order for that customer
    EXECUTE @OrderID = [Sales].[uspPlaceNewOrder] @CustomerID, @Amount, @OrderDate, @Status;
    
    -- fill the order for that customer
    EXECUTE @RC = [Sales].[uspFillOrder] @OrderID, @FilledDate;
    
    COMMIT TRANSACTION
    
  9. 在**“文件”菜单上,单击“全部保存”**。

    此时,已准备好运行测试了。

运行数据库单元测试

  1. 在**“测试视图”中,右击“Sales_uspCancelOrderTest”,然后单击“运行选定内容”**。

  2. 在**“测试结果”**窗口中查看结果。

    测试失败,将出现以下错误:

    Test method TestProject1.DatabaseUnitTests1.Sales_uspCancelOrderTest threw exception: System.Data.SqlClient.SqlException: You can only cancel open orders.

    接下来,修改代码以指示期望出现异常。

修改单元测试的代码

  1. 在**“解决方案资源管理器”中,展开“TestProject1”,右击“DatabaseUnitTests1.cs”,然后单击“查看代码”**。

  2. 在代码编辑器中,导航到 Sales_uspCancelOrderTest 方法。 修改方法的特性,使之与以下代码相符:

            [TestMethod(), ExpectedSqlException(Severity=16, MatchFirstError=false, State=1)]
            public void Sales_uspCancelOrderTest()
    

    您指定期望看到特定 SQL 异常。 可以选择指定特定错误编号。 如果不添加此特性,单元测试将失败,“测试结果”窗口中将显示一条消息。

  3. 在“文件”菜单上,单击“保存 DatabaseUnitTests1.cs”。

    接下来,重新运行单元测试以验证其是否按预期失败。

重新运行数据库单元测试

  1. 在**“测试视图”中,右击“Sales_uspCancelOrderTest”,然后单击“运行选定内容”**。

  2. 在**“测试结果”**窗口中查看结果。

    测试通过,这意味着过程在假定失败时失败。

后续步骤

在典型的项目中,将定义其他单元测试,以便验证所有的关键数据库对象是否正常工作。 在完成这组测试之后,将这些测试签入版本控制中,以便与团队中的其他成员共享这些测试。

在建立了基线之后,可以创建和修改数据库对象,然后创建相关测试,以验证所做的更改是否将中断期望的行为。

请参见

任务

如何:创建空的数据库单元测试

如何:配置数据库单元测试执行

概念

创建和定义数据库单元测试

使用单元测试验证数据库代码

使用数据生成器生成数据库的测试数据