sql >> データベース >  >> RDS >> Database

データベース単体テストで依存関係とデータを分離する技術

    すべてのデータベース開発者は、データベースユニットテストを作成します。これは、バグを早期に検出するのに役立つだけでなく、データベースオブジェクトの予期しない動作が本番環境の問題になる場合に多くの時間と労力を節約します。

    現在、tSQLtなどのデータベース単体テストフレームワークと、dbForge単体テストなどのサードパーティの単体テストツールが多数あります。

    一方では、サードパーティのテストツールを使用する利点は、開発チームが追加機能を備えた単体テストを即座に作成して実行できることです。また、テストフレームワークを直接使用すると、単体テストをより細かく制御できます。したがって、単体テストフレームワーク自体に機能を追加できます。ただし、この場合、チームにはこれを行うための時間と一定レベルの専門知識が必要です。

    この記事では、データベースの単体テストの記述方法を改善するのに役立ついくつかの標準的な方法について説明します。

    まず、データベースの単体テストのいくつかの重要な概念を見ていきましょう。

    データベース単体テストとは

    Dave Greenによると、データベース単体テストでは、テーブル、ビュー、ストアドプロシージャなど、データベースの小さな単体が期待どおりに機能していることを確認します。

    データベース単体テストは、コードがビジネス要件を満たしているかどうかを検証するために作成されています。

    たとえば、「図書館員(エンドユーザー)が図書館(経営情報システム)に新しい本を追加できるようにする必要がある」などの要件を受け取った場合、保存された手順にユニットテストを適用して、 に新しい本を追加できます テーブル。

    場合によっては、一連の単体テストにより、コードが要件を満たしていることが確認されます。したがって、tSQLtを含むほとんどの単体テストフレームワークでは、個別のテストを実行するのではなく、関連する単体テストを単一のテストクラスにグループ化できます。

    AAAの原則

    単体テストを作成するための標準的な方法である単体テストの3ステップの原則について言及する価値があります。 AAAの原則は単体テストの基礎であり、次の手順で構成されています。

    1. 配置/組み立て
    2. 行為
    3. アサート

    アレンジ セクションは、データベースの単体テストを作成するための最初のステップです。期待される結果をテストおよび設定するためのデータベースオブジェクトの構成について説明します。

    セクションは、実際の出力を生成するために(テスト中の)データベースオブジェクトが呼び出されるときです。

    アサート stepは、実際の出力を期待される出力に一致させることを扱い、テストが成功するか失敗するかを検証します。

    特定の例でこれらの方法を調べてみましょう。

    AddProductを確認するための単体テストを作成する場合 ストアドプロシージャは新しい製品を追加できます。製品を設定します および期待される製品 製品が追加された後のテーブル。この場合、メソッドは[配置/組み立て]セクションにあります。

    AddProductプロシージャを呼び出し、結果をProductテーブルに入れることは、Actセクションでカバーされています。

    Assert部分は、ProductテーブルをExpectedProductテーブルと単純に照合して、ストアドプロシージャが正常に実行されたか失敗したかを確認します。

    単体テストの依存関係を理解する

    これまで、データベースの単体テストの基本と、標準の単体テストを作成する際のAAA(Assemble、Act、およびAssert)の原則の重要性について説明してきました。

    それでは、パズルのもう1つの重要な部分である単体テストの依存関係に焦点を当てましょう。

    AAAの原則に従い、特定のデータベースオブジェクト(テスト中)のみに焦点を当てるだけでなく、単体テストに影響を与える可能性のある依存関係も知る必要があります。

    依存関係を理解する最良の方法は、単体テストの例を見ることです。

    EmployeesSampleデータベースのセットアップ

    次に進むには、サンプルデータベースを作成し、それを EmployeesSampleと呼びます。 :

    -- Create the Employees sample database to demonstrate unit testing
    
    CREATE DATABASE EmployeesSample;
    GO
    

    次に、従業員を作成します サンプルデータベースのテーブル:

    -- Create the Employee table in the sample database
    
    USE EmployeesSample
    
    CREATE TABLE Employee
      (EmployeeId INT PRIMARY KEY IDENTITY(1,1),
      NAME VARCHAR(40),
      StartDate DATETIME2,
      Title VARCHAR(50)
      );
    GO

    サンプルデータの入力

    いくつかのレコードを追加してテーブルにデータを入力します:

    -- Adding data to the Employee table
    INSERT INTO Employee (NAME, StartDate, Title)
      VALUES 
      ('Sam','2018-01-01', 'Developer'),
      ('Asif','2017-12-12','Tester'),
      ('Andy','2016-10-01','Senior Developer'),
      ('Peter','2017-11-01','Infrastructure Engineer'),
      ('Sadaf','2015-01-01','Business Analyst');
    GO
    

    表は次のようになります:

    -- View the Employee table
    
      SELECT e.EmployeeId
            ,e.NAME
            ,e.StartDate
            ,e.Title FROM  Employee e;
    GO
    

    この記事では、dbForge Studio forSQLServerを使用していることに注意してください。したがって、SSMS(SQL Server Management Studio)で同じコードを実行すると、出力の外観が異なる場合があります。スクリプトとその結果に関しては違いはありません。

    新しい従業員を追加するための要件

    現在、新しい従業員を追加する要件を受け取った場合、要件を満たすための最良の方法は、テーブルに新しい従業員を正常に追加できるストアドプロシージャを作成することです。

    これを行うには、次のようにAddEmployeeストアドプロシージャを作成します。

    -- Stored procedure to add a new employee 
    
    CREATE PROCEDURE AddEmployee @Name VARCHAR(40),
    @StartDate DATETIME2,
    @Title VARCHAR(50)
    AS
    BEGIN
      SET NOCOUNT ON
        INSERT INTO Employee (NAME, StartDate, Title)
      VALUES (@Name, @StartDate, @Title);
    END
    

    要件が満たされているかどうかを確認するための単体テスト

    AddEmployeeストアドプロシージャがEmployeeテーブルに新しいレコードを追加するための要件を満たしているかどうかを確認するデータベース単体テストを作成します。

    テストフレームワークやサードパーティの単体テストツールを使用して単体テストを作成するのではなく、単体テストコードをシミュレートすることで、単体テストの哲学を理解することに焦点を当てましょう。

    ユニットテストのシミュレーションとSQLでのAAA原則の適用

    最初に行う必要があるのは、単体テストフレームワークを使用しないため、SQLでAAAの原則を模倣することです。

    アセンブルセクションは、実際のテーブルと期待されるテーブルが通常設定され、期待されるテーブルにデータが入力されるときに適用されます。このステップでは、SQL変数を使用して、期待されるテーブルを初期化できます。

    Actセクションは、実際のストアドプロシージャが呼び出されて、実際のテーブルにデータが挿入されるときに使用されます。

    アサートセクションは、期待されるテーブルが実際のテーブルと一致する場合です。アサート部分のシミュレーションは少し注意が必要で、次の手順で実行できます。

    • 1である必要がある2つのテーブル間の共通の(一致する)行をカウントします(期待されるテーブルには実際のテーブルと一致する必要があるレコードが1つしかないため)
    • 期待されるテーブルレコードから実際のテーブルレコードを除外することは0に等しい必要があります(期待されるテーブルのレコードが実際のテーブルにも存在する場合、期待されるテーブルからすべての実際のテーブルレコードを除外すると0が返されます)
    • >

    SQLスクリプトは次のとおりです。

    [expand title =” Code”]

    -- Simulating unit test to test the AddEmployee stored procedure
    
    CREATE PROCEDURE TestAddEmployee
    AS
    BEGIN
      -- (1) Assemble
    
      -- Set up new employee data
      DECLARE @EmployeeId INT = 6
             ,@NAME VARCHAR(40) = 'Adil'
             ,@StartDate DATETIME2 = '2018-03-01'
             ,@Title VARCHAR(50) = 'Development Manager'
    
    
      -- Set up the expected table
      CREATE TABLE #EmployeeExpected (
        EmployeeId INT PRIMARY KEY IDENTITY (6, 1) 
        -- the expected table EmployeeId should begin with 6 
        -- since the actual table has already got 5 records and 
        -- the next EmployeeId in the actual table is 6
       ,NAME VARCHAR(40)
       ,StartDate DATETIME2
       ,Title VARCHAR(50)
      );
    
      -- Add the expected table data
      INSERT INTO #EmployeeExpected (NAME, StartDate, Title)
        VALUES (@NAME, @StartDate, @Title);
    
      -- (2) Act
    
      -- Call AddEmployee to add new employee data to the Employee table
      INSERT INTO Employee
      EXEC AddEmployee @NAME
                      ,@StartDate
                      ,@Title
    
    
    
      -- (3) Assert
    
      -- Match the actual table with the expected table
      DECLARE @ActualAndExpectedTableCommonRecords INT = 0 -- we assume that expected and actual table records have nothing in common
    
      SET @ActualAndExpectedTableCommonRecords = (SELECT
          COUNT(*)
        FROM (SELECT
            e.EmployeeId
           ,e.NAME
           ,e.StartDate
           ,e.Title
          FROM Employee e
          INTERSECT
          SELECT
            ee.EmployeeId
           ,ee.NAME
           ,ee.StartDate
           ,ee.Title
          FROM #EmployeeExpected ee) AS A)
    
    
      DECLARE @ExpectedTableExcluldingActualTable INT = 1 -- we assume that expected table has records which do not exist in the actual table
    
      SET @ExpectedTableExcluldingActualTable = (SELECT
          COUNT(*)
        FROM (SELECT
            ee.EmployeeId
           ,ee.NAME
           ,ee.StartDate
           ,ee.Title
          FROM #EmployeeExpected ee
          EXCEPT
          SELECT
            e.EmployeeId
           ,e.NAME
           ,e.StartDate
           ,e.Title
          FROM Employee e) AS A)
    
    
      IF @ActualAndExpectedTableCommonRecords = 1
        AND @ExpectedTableExcluldingActualTable = 0
        PRINT '*** Test Passed! ***'
      ELSE
        PRINT '*** Test Failed! ***'
    
    END
    

    [/エキスパンド]

    シミュレートされた単体テストの実行

    ストアドプロシージャが作成されたら、シミュレートされた単体テストで実行します。

    -- Running simulated unit test to check the AddEmployee stored procedure
    EXEC TestAddEmployee
    

    出力は次のとおりです。

    おめでとう!データベース単体テストに合格しました。

    単体テストでの依存関係の形での問題の特定

    作成されて正常に実行されたにもかかわらず、作成した単体テストで何か問題を検出できますか?

    単体テストのセットアップ(アセンブル部分)をよく見ると、期待されるテーブルにはID列との不要なバインディングがあります:

    単体テストを作成する前に、実際の(従業員)テーブルに5つのレコードを追加しました。したがって、テストセットアップでは、期待されるテーブルのID列は6で始まります。ただし、これは、実際の(Employee)テーブルに5つのレコードがあり、期待されるテーブル(#EmployeeExpected)と一致することを常に期待していることを意味します。

    これが単体テストにどのように影響するかを理解するために、実際の(従業員)テーブルを今すぐ見てみましょう:

    Employeeテーブルに別のレコードを追加します:

    -- Adding a new record to the Employee table
    
    INSERT INTO Employee (NAME, StartDate, Title)
      VALUES ('Mark', '2018-02-01', 'Developer');
    

    今すぐ従業員テーブルを見てください:

    EmpoyeeId 6(Adil)を削除して、以前に保存されたレコードではなく、独自のバージョンのEmployeeId 6(Adil)に対して単体テストを実行できるようにします。

    -- Deleting the previously created EmployeeId: 6 (Adil) record from the Employee table
    
    DELETE FROM Employee
      WHERE EmployeeId=6
    

    シミュレートされた単体テストを実行し、結果を確認します。

    -- Running the simulated unit test to check the AddEmployee stored procedure
    
    EXEC TestAddEmployee
    
    を確認します

    今回はテストに失敗しました。答えは、以下に示すように、従業員テーブルの結果セットにあります。

    新しいレコードを追加し、以前に追加した従業員レコードを削除した後で単体テストを再実行すると、上記の単体テストでの従業員IDのバインドは機能しません。

    テストには3つのタイプの依存関係があります:

    1. データの依存関係
    2. キー制約の依存関係
    3. ID列の依存関係

    データの依存関係

    まず、この単体テストはデータベース内のデータに依存します。 Dave Greenによると、単体テストデータベースに関しては、データ自体が依存関係にあります。

    これは、データベースの単体テストがデータベース内のデータに依存してはならないことを意味します。たとえば、単体テストには、削除または変更できるデータベースにすでに存在するデータに依存するのではなく、データベースオブジェクト(テーブル)に挿入される実際のデータを含める必要があります。

    私たちの場合、5つのレコードが実際の従業員テーブルにすでに挿入されているという事実はデータの依存関係であり、コードのユニットのみがテストされるという単体テストの哲学に違反してはならないため、防止する必要があります。

    >

    つまり、テストデータはデータベース内の実際のデータに依存するべきではありません。

    キー制約の依存関係

    もう1つの依存関係は、キー制約の依存関係です。これは、主キー列のEmployeeIdも依存関係であることを意味します。良い単体テストを書くためには、それを防ぐ必要があります。ただし、主キーの制約をテストするには、個別の単体テストが必要です。

    たとえば、AddEmployeeストアドプロシージャをテストするには、Employeeテーブルの主キーを削除して、主キーに違反する心配なしにオブジェクトをテストできるようにする必要があります。

    ID列の依存関係

    主キー制約と同様に、ID列も依存関係です。したがって、AddEmployeeプロシージャのID列の自動インクリメントロジックをテストする必要はありません。絶対に避けなければなりません。

    単体テストでの依存関係の分離

    テーブルから制約を一時的に削除し、単体テストでデータベース内のデータに依存しないようにすることで、3つの依存関係すべてを防ぐことができます。これは、標準のデータベース単体テストの記述方法です。

    この場合、Employeeテーブルのデータがどこから来たのかを尋ねる場合があります。答えは、単体テストで定義されたテストデータがテーブルに入力されることです。

    単体テストのストアドプロシージャの変更

    単体テストで依存関係を削除しましょう:

    [expand title =” Code”]

    -- Simulating dependency free unit test to test the AddEmployee stored procedure
    ALTER PROCEDURE TestAddEmployee
    AS
    BEGIN
      -- (1) Assemble
    
      -- Set up new employee data
      DECLARE @NAME VARCHAR(40) = 'Adil'
             ,@StartDate DATETIME2 = '2018-03-01'
             ,@Title VARCHAR(50) = 'Development Manager'
    
      -- Set actual table
      DROP TABLE Employee -- drop table to remove dependencies
    
      CREATE TABLE Employee -- create a table without dependencies (PRIMARY KEY and IDENTITY(1,1))
      (
        EmployeeId INT DEFAULT(0)
       ,NAME VARCHAR(40)
       ,StartDate DATETIME2
       ,Title VARCHAR(50)
      )
    
      -- Set up the expected table without dependencies (PRIMARY KEY and IDENTITY(1,1)
      CREATE TABLE #EmployeeExpected (
        EmployeeId INT DEFAULT(0)
       ,NAME VARCHAR(40)
       ,StartDate DATETIME2
       ,Title VARCHAR(50)
      )
    
      -- Add the expected table data
      INSERT INTO #EmployeeExpected (NAME, StartDate, Title)
        VALUES (@NAME, @StartDate, @Title)
    
      -- (2) Act
    
      -- Call AddEmployee to add new employee data to the Employee table
      EXEC AddEmployee @NAME
                      ,@StartDate
                      ,@Title
     
      -- (3) Assert
    
      -- Match the actual table with the expected table
      DECLARE @ActualAndExpectedTableCommonRecords INT = 0 -- we assume that the expected and actual table records have nothing in common
    
      SET @ActualAndExpectedTableCommonRecords = (SELECT
          COUNT(*)
        FROM (SELECT
            e.EmployeeId
           ,e.NAME
           ,e.StartDate
           ,e.Title
          FROM Employee e
          INTERSECT
          SELECT
            ee.EmployeeId
           ,ee.NAME
           ,ee.StartDate
           ,ee.Title
          FROM #EmployeeExpected ee) AS A)
    
    
      DECLARE @ExpectedTableExcluldingActualTable INT = 1 -- we assume that the expected table has records which donot exist in actual table
    
      SET @ExpectedTableExcluldingActualTable = (SELECT
          COUNT(*)
        FROM (SELECT
            ee.EmployeeId
           ,ee.NAME
           ,ee.StartDate
           ,ee.Title
          FROM #EmployeeExpected ee
          EXCEPT
          SELECT
            e.EmployeeId
           ,e.NAME
           ,e.StartDate
           ,e.Title
          FROM Employee e) AS A)
    
    
      IF @ActualAndExpectedTableCommonRecords = 1
        AND @ExpectedTableExcluldingActualTable = 0
        PRINT '*** Test Passed! ***'
      ELSE
        PRINT '*** Test Failed! ***'
    
      -- View the actual and expected tables before comparison
        SELECT e.EmployeeId
              ,e.NAME
              ,e.StartDate
              ,e.Title FROM Employee e
    
          SELECT    ee.EmployeeId
                   ,ee.NAME
                   ,ee.StartDate
                   ,ee.Title FROM #EmployeeExpected ee
      
      -- Reset the table (Put back constraints after the unit test)
      DROP TABLE Employee
      DROP TABLE #EmployeeExpected
    
      CREATE TABLE Employee (
        EmployeeId INT PRIMARY KEY IDENTITY (1, 1)
       ,NAME VARCHAR(40)
       ,StartDate DATETIME2
       ,Title VARCHAR(50)
      );
    
    END

    [/エキスパンド]

    依存関係のないシミュレートされた単体テストの実行

    シミュレートされた単体テストを実行して、結果を確認します。

    -- Running the dependency-free simulated unit test to check the AddEmployee stored procedure
    
    EXEC TestAddEmployee
    

    単体テストを再実行して、AddEmployeeストアドプロシージャを確認します。

    -- Running the dependency-free simulated unit test to check the AddEmployee stored procedure
    
    EXEC TestAddEmployee
    

    おめでとう!単体テストからの依存関係は正常に削除されました。

    これで、新しいレコードまたは新しいレコードのセットをEmployeeテーブルに追加しても、データと制約の依存関係がテストから正常に削除されたため、単体テストに影響を与えることはありません。

    tSQLtを使用したデータベース単体テストの作成

    次のステップは、シミュレートされた単体テストに基づいて実際のデータベース単体テストを作成することです。

    SSMS(SQL Server Management Studio)を使用している場合は、単体テストを作成して実行する前に、tSQLtフレームワークをインストールし、テストクラスを作成し、CLRを有効にする必要があります。

    dbForge Studio for SQL Serverを使用している場合は、以下に示すように、AddEmployeeストアドプロシージャを右クリックし、[単体テスト]=>[新しいテストの追加...]をクリックして単体テストを作成できます。

    新しいテストを追加するには、必要な単体テスト情報を入力します。

    単体テストを作成するには、次のスクリプトを使用します。

    --  Comments here are associated with the test.
    --  For test case examples, see: http://tsqlt.org/user-guide/tsqlt-tutorial/
    CREATE PROCEDURE [BasicTests].[test if new employee can be added]
    AS
    BEGIN
      --Assemble
      DECLARE @NAME VARCHAR(40) = 'Adil'
             ,@StartDate DATETIME2 = '2018-03-01'
             ,@Title VARCHAR(50) = 'Development Manager'
    
    
      EXEC tSQLt.FakeTable "dbo.Employee" -- This will create a dependency-free copy of the Employee table
      
      CREATE TABLE BasicTests.Expected -- Create the expected table
      (
        EmployeeId INT 
        ,NAME VARCHAR(40)
       ,StartDate DATETIME2
       ,Title VARCHAR(50)
      )
    
    
      -- Add the expected table data
      INSERT INTO BasicTests.Expected (NAME, StartDate, Title)
        VALUES (@NAME, @StartDate, @Title)
    
      --Act
      EXEC AddEmployee @Name -- Insert data into the Employee table
                      ,@StartDate 
                      ,@Title 
      
    
      --Assert 
      EXEC tSQLt.AssertEqualsTable @Expected = N'BasicTests.Expected'
                                  ,@Actual = N'dbo.Employee'
                                  ,@Message = N'Actual table matched with expected table'
                                  ,@FailMsg = N'Actual table does not match with expected table'
    
    END;
    GO

    次に、データベースの単体テストを実行します。

    おめでとう!依存関係のないデータベース単体テストの作成と実行に成功しました。

    やるべきこと

    それでおしまい。この記事を読んだ後は、データベース単体テストから依存関係を分離し、データと制約の依存関係のないデータベース単体テストを作成する準備ができています。その結果、次のことを実行してスキルを向上させることができます。

    1. 従業員の削除ストアドプロシージャを追加して、依存関係のある従業員の削除のシミュレートされたデータベース単体テストを作成して、特定の条件下で失敗するかどうかを確認してください。
    2. 従業員の削除ストアドプロシージャを追加し、依存関係のないデータベース単体テストを作成して、従業員を削除できるかどうかを確認してください
    3. Search Employeeストアドプロシージャを追加し、依存関係を使用してシミュレートされたデータベース単体テストを作成して、従業員を検索できるかどうかを確認してください。
    4. Search Employeeストアドプロシージャを追加し、依存関係のないデータベース単体テストを作成して、従業員を検索できるかどうかを確認してください。
    5. 要件を満たすストアドプロシージャを作成し、依存関係のないデータベース単体テストを作成して、テストに合格するか失敗するかを確認することで、より複雑な要件を試してください。ただし、テストが繰り返し可能であり、コードの単位のテストに焦点を合わせていることを確認してください

    便利なツール:

    dbForge単体テスト– SQL ServerManagementStudioで自動単体テストを実装するための直感的で便利なGUI。


    1. 主キーを整数からシリアルに変換する方法は?

    2. SQLServerで複数のスペースの文字列を返す3つの方法

    3. Oracleで先行ゼロを使用して数値をフォーマットする2つの方法

    4. HibernateはデータベースからSequenceInformationをフェッチできませんでした