diff --git a/.gitignore b/.gitignore index 57a9983e2..97e030cd1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ /tSQLtCLR/OfficialSigningKey/KeyInfoC.txt .vscode/settings.json +/.vs diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..e2c75648d --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,9 @@ +theme: jekyll-theme-dinky +title: tSQLt - Database Unit Testing for SQL Server +description: tSQLt is the open source framework for unit testing on SQL Server +defaults: + - + scope: + path: "" + values: + layout: "default" \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index abec8fe39..c3d73ad26 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,3 @@ -WELCOME TO TSQLT, THE OPEN SOURCE DATABASE UNIT TESTING FRAMEWORK FOR SQL SERVER \ No newline at end of file +--- +--- +WELCOME TO TSQLT, THE OPEN SOURCE DATABASE UNIT TESTING FRAMEWORK FOR SQL SERVER diff --git a/docs/user-guide/assertions/assertemptytable.md b/docs/user-guide/assertions/assertemptytable.md new file mode 100644 index 000000000..7e9f5e4b3 --- /dev/null +++ b/docs/user-guide/assertions/assertemptytable.md @@ -0,0 +1,57 @@ +# AssertEmptyTable + +## Syntax + +``` sql +tSQLt.AssertEmptyTable [@TableName = ] 'name of table to be checked' [, [@Message = ] 'message' ] +``` + +## Arguments + +[**@TableName** = ] name of table to be checked + +The name of a table which is expected to be empty. @TableName is NVARCHAR(MAX) with no default. + +[**@Message** = ] ‘message’ + +Optional. String containing an additional failure message to be used if the expected and actual values are not equal. @Message is NVARCHAR(MAX) with a default of ‘unexpected/missing resultset rows!’. + +## Return Code Values + +Returns 0 + +## Errors Raised + +Raises a `failure` error if the table contains any rows. + +## Result Sets +None + +## Overview + +AssertEmptyTable succeeds when a table is empty, fails otherwise. The failure message displays all rows that are found. + +## Examples + +This example uses AssertEmptyTable to check that a table-valued function returns an empty resultset. + +``` sql +CREATE PROCEDURE testFinancialApp.[test that Report generates no rows if base tables are empty] +AS +BEGIN + IF OBJECT_ID('actual') IS NOT NULL DROP TABLE actual; + +------Fake Table + EXEC tSQLt.FakeTable 'FinancialApp', 'CurrencyConversion'; + EXEC tSQLt.FakeTable 'FinancialApp', 'Sales'; + +------Execution + SELECT amount, currency, customerId, employeeId, itemId, date + INTO actual + FROM FinancialApp.Report('USD'); + +------Assertion + EXEC tSQLt.AssertEmptyTable 'actual'; +END; +GO +``` diff --git a/docs/user-guide/assertions/assertequals.md b/docs/user-guide/assertions/assertequals.md new file mode 100644 index 000000000..512ebc44e --- /dev/null +++ b/docs/user-guide/assertions/assertequals.md @@ -0,0 +1,93 @@ +# AssertEquals + +## Syntax + +``` sql +tSQLt.AssertEquals [@expected = ] expected value, [@actual = ] actual value[, [@message = ] 'message' ] +``` + +## Arguments +[**@Expected** = ] expected value + +The expected value for the test. @Expected is SQL_VARIANT with no default. + +[**@Actual** = ] actual value + +The actual value resulting from processing during the test. @Actual is SQL_VARIANT with no default. + +[**@Message** = ] ‘message’ + +Optional. String containing an additional failure message to be used if the expected and actual values are not equal. @Message is NVARCHAR(MAX) with no default. + +## Return Code Values +Returns 0 + +## Error Raised +Raises a `failure` error if expected and actual are not equal. + +Raises an `Operand type clash` error if the value passed for @Expected or @Actual is not compatible with SQL_VARIANT. The most common form of this is VARCHAR(MAX) – for this `tSQLt.AssertEqualsString` is provided. + +## Result Sets +None + +## Overview +`tSQLt.AssertEquals` compares two values for equality. If they are not equal, the test case is failed; otherwise, `tSQLt.AssertEquals` does not affect test processing. For the purposes of `tSQLt.AssertEquals`, NULL is considered equal to NULL. Any non-NULL value is considered not equal to NULL. + +## Examples +### Example: `tSQLt.AssertEquals` to check the results of a function +This test case uses `tSQLt.AssertEquals` to compare the return result of a function with an expected value. + +``` sql +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 +``` + +### Example: A variety of the possibilities of `tSQLt.AssertEquals` + +The examples below show what to expect from AssertEquals when called with different values. + +``` sql + EXEC tSQLt.AssertEquals 12345.6789, 12345.6789; -- pass + EXEC tSQLt.AssertEquals 'hello', 'hello'; -- pass + EXEC tSQLt.AssertEquals N'hello', N'hello'; -- pass + + DECLARE @datetime DATETIME; SET @datetime = CAST('12-13-2005' AS DATETIME); + EXEC tSQLt.AssertEquals @datetime, @datetime; -- pass + + DECLARE @bit BIT; SET @bit = CAST(1 AS BIT); + EXEC tSQLt.AssertEquals @bit, @bit; -- pass + + EXEC tSQLt.AssertEquals NULL, NULL; -- pass + EXEC tSQLt.AssertEquals 17, NULL; -- fail + EXEC tSQLt.AssertEquals NULL, 17; -- fail + + EXEC tSQLt.AssertEquals 12345.6789, 54321.123; -- fail + EXEC tSQLt.AssertEquals 'hello', 'goodbye'; -- fail + + DECLARE @datetime1 DATETIME; SET @datetime1 = CAST('12-13-2005' AS DATETIME); + DECLARE @datetime2 DATETIME; SET @datetime2 = CAST('07-19-2005' AS DATETIME); + EXEC tSQLt.AssertEquals @datetime1, @datetime2; -- fail + + DECLARE @bit1 BIT; SET @bit1 = CAST(1 AS BIT); + DECLARE @bit2 BIT; SET @bit2 = CAST(1 AS BIT); + EXEC tSQLt.AssertEquals @bit1, @bit2; -- pass +``` \ No newline at end of file diff --git a/docs/user-guide/assertions/assertequalsstring.md b/docs/user-guide/assertions/assertequalsstring.md new file mode 100644 index 000000000..85e09166a --- /dev/null +++ b/docs/user-guide/assertions/assertequalsstring.md @@ -0,0 +1,65 @@ +# AssertEqualsString + +## Syntax + +``` sql +tSQLt.AssertEqualsString [@expected = ] expected value + , [@actual = ] actual value + [, [@message = ] 'message' ] +``` + +## Arguments +[**@Expected** = ] expected value + +The expected value for the test. @Expected is NVARCHAR(MAX) with no default. + +[**@Actual** = ] actual value + +The actual value resulting from processing during the test. @Actual is NVARCHAR(MAX) with no default. + +[**@Message** = ] ‘message’ + +Optional. String containing an additional failure message to be used if the expected and actual values are not equal. @Message is NVARCHAR(MAX) with no default. + +## Return Code Values +Returns 0 + +## Error Raised +Raises a `failure` error if expected and actual are not equal. + +## Result Sets +None + +## Overview +`tSQLt.AssertEqualsString` compares two string values for equality. If they are not equal, the test case is failed; otherwise, `tSQLt.AssertEqualsString` does not affect test processing. For the purposes of `tSQLt.AssertEqualsString`, NULL is considered equal to NULL. Any non-NULL value is considered not equal to NULL. + +## Examples +### Example: AssertEqualsString to check the results of a function +This test case uses AssertEqualsString to compare the return result of a function with an expected value. The function formats a first and last name into a standard string for display. + +``` sql +CREATE PROC TestPerson.[test FormatName concatenates names correctly] +AS +BEGIN + DECLARE @expected NVARCHAR(MAX); SET @expected = 'Smith, John'; + DECLARE @actual NVARCHAR(MAX); + + SELECT @actual = person.FormatName('John', 'Smith'); + + EXEC tSQLt.AssertEqualsString @expected, @actual; +END; +``` + +### Example: A variety of the possibilities of AssertEqualsString + +The examples below show what to expect from AssertEqualsString when called with different values. + +``` sql +EXEC tSQLt.AssertEqualsString 'hello', 'hello'; -- pass +EXEC tSQLt.AssertEqualsString N'goodbye', N'goodbye'; -- pass +EXEC tSQLt.AssertEqualsString 'hello', N'hello'; - pass (values are compared as NVARCHAR(MAX) + +EXEC tSQLt.AssertEqualsString 'hello', NULL; -- fail +EXEC tSQLt.AssertEqualsString NULL, 'hello'; -- fail +EXEC tSQLt.AssertEqualsString NULL, NULL; -- pass +``` diff --git a/docs/user-guide/assertions/assertequalstable.md b/docs/user-guide/assertions/assertequalstable.md new file mode 100644 index 000000000..71a5e0924 --- /dev/null +++ b/docs/user-guide/assertions/assertequalstable.md @@ -0,0 +1,117 @@ +# AssertEqualsTable + +## Syntax + +``` sql +tSQLt.AssertEqualsTable [@Expected = ] 'expected table name' + , [@Actual = ] 'actual table name' + [, [@FailMsg = ] 'message' ] +``` + +## Arguments +[**@Expected** = ] expected table name + +The name of a table which contains the expected results for the test. @Expected is NVARCHAR(MAX) with no default. + +[**@Actual** = ] actual table name + +The name of a table which contains the results from processing during the test. @Actual is NVARCHAR(MAX) with no default. + +[**@FailMsg** = ] ‘message’ + +Optional. String containing an additional failure message to be used if the expected and actual values are not equal. @FailMsg is NVARCHAR(MAX) with a default of ‘unexpected/missing resultset rows!’. + +## Return Code Values +Returns 0 + +## Errors Raised +Raises a `failure` error if the contents of the expected table and the actual table are not equal. + +Certain datatypes cannot be compared with `tSQLt.AssertEqualsTable`. If the tables being compared contain an unsupported datatype, the following error will be raised: + + > The table contains a datatype that is not supported for `tSQLt.AssertEqualsTable`. + +The following datatypes are known to be unsupported by `tSQLt.AssertEqualsTable`: text, ntext, image, xml, geography, geometry, rowversion and CLR datatypes that are not marked comparable and byte ordered. + +## Result Sets +None + +## Overvoew +`tSQLt.AssertEqualsTable` compares the contents of two tables for equality. It does this by comparing each row of the tables for an exact match on all columns. If the tables do not contain the same data, the failure message displays which rows could not be matched. + +## Examples +### Example: AssertEqualsTable to check the results of a view +This test case uses AssertEqualsTable to compare the data returned by a view to an expected data set. + +``` sql +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 +``` + +## Understanding The Output +When the two tables being compared contain different data, the results are displayed as a text table in the failure message. The first column of this text table (\_m\_) describes the result of the comparison. The symbol “<” indicates that the row was found in the Expected table but did not match anything in the Actual table. The symbol “>” indicates that the row was found in the Actual table but not in the Expected table. Finally, the symbol “=” indicates that the row was matched between the Expected and Actual tables. + +For example, consider the following Expected and Actual tables: + +Expected +|col1|col2|col3| +|---|---|---| +|1|A|a| +|2|B|b| +|3|C|c| + +Actual +|col1|col2|col3| +|---|---|---| +|1|A|a| +|3|X|c| + +These tables would result in the following failure message: + + +`failed: unexpected/missing resultset rows!` +|_m_|col1|col2|col3| +|---|---|---|---| +|< |2 |B |b | +|< |3 |C |c | +|= |1 |A |a | +|> |3 |X |c | diff --git a/docs/user-guide/assertions/assertequalstableschema.md b/docs/user-guide/assertions/assertequalstableschema.md new file mode 100644 index 000000000..87c113de2 --- /dev/null +++ b/docs/user-guide/assertions/assertequalstableschema.md @@ -0,0 +1,36 @@ +# AssertEqualsTableSchema + +## Syntax + +``` sql +tSQLt.AssertEqualsTableSchema [@Expected = ] 'expected table name' + , [@Actual = ] 'actual table name' + [, [@FailMsg = ] 'message' ] +``` + +## Arguments +[**@Expected** = ] expected table name + +The name of a table with the expected columns (and data types). @Expected is NVARCHAR(MAX) with no default. + +[**@Actual** = ] actual table name + +The name of a table created as the result from processing during the test. @Actual is NVARCHAR(MAX) with no default. + +[**@FailMsg** = ] ‘message’ + +Optional. String containing an additional failure message to be used if the expected and actual values are not equal. @FailMsg is NVARCHAR(MAX) with a default of ‘unexpected/missing resultset rows!’. + +## Return Code Values +Returns 0 + +## Errors Raised +Raises a `failure` error if the contents of the expected table and the actual table are not equal. + +## Result Sets +None + +## Overview +`tSQLt.AssertEqualsTableSchema` works like `tSQLt.AssertEqualsTable`, but it compares the table structure instead of the table contents. + +Under the hood, `tSQLt.AssertEqualsTableSchema` calls `tSQLt.AssertEqualsTable` on the table metadata. For details of how to interpret its output, check out the `tSQLt.AssertEqualsTable` [documentation](assertequalstable.md). diff --git a/docs/user-guide/assertions/assertlike.md b/docs/user-guide/assertions/assertlike.md new file mode 100644 index 000000000..36e9faa58 --- /dev/null +++ b/docs/user-guide/assertions/assertlike.md @@ -0,0 +1,74 @@ +# AssertLike + +## Syntax + +``` sql +tSQLt.AssertLike [@ExpectedPattern = ] expected pattern + , [@Actual = ] actual value + [, [@Message = ] 'message' ] +``` + +## Arguments + +[**@ExpectedPattern** = ] expected pattern + +An NVARCHAR pattern that may contain regular characters and wildcard characters. ExpectedPattern uses the same pattern syntax as the SQL LIKE keyword. + +[**@Actual** = ] actual value + +The actual value resulting from processing during the test. @Actual is an NVARCHAR(MAX) with no default. + +[**@Message** = ] ‘message’ + +Optional. String containing an additional failure message to be used if ExpectedPattern does not match the specified actual value. @message is NVARCHAR(MAX) with no default. + +## Return Code Values + +Returns 0 + +## Error Raised + +Raises a `failure` error if the expected pattern does not match the actual value provided. + +Raises an `@ExpectedPattern may not exceed 4000 characters` error if the value passed for @ExpectedPattern is more than 4000 characters in length. This is due to a limitation in SQL Server’s LIKE keyword. + +## Result Sets + +None + +## Overview + +`tSQLt.AssertLike` checks if the actual value matches the expected pattern. If it does not match, the test case is failed; otherwise, `tSQLt.AssertLike` does not affect test processing. For the purposes of `tSQLt.AssertLike`, NULL is considered LIKE to NULL. Any non-NULL value is considered not LIKE to NULL. + +## Examples + +### Example: AssertLike to check the results of a function + +This test case uses AssertLike to compare the return result of a function with an expected value. + +``` sql +CREATE PROC TestPerson.[test FormatName concatenates names correctly] +AS +BEGIN + DECLARE @actual NVARCHAR(MAX); + + SELECT @actual = person.FormatName('John', 'Smith'); + + EXEC tSQLt.AssertLike 'John%Smith', @actual; +END; +``` + +### Example: A variety of the possibilities of AssertLike + +The examples below show what to expect from AssertLike when called with different values. + +``` sql +EXEC tSQLt.AssertLike 'hello', 'hello'; -- pass +EXEC tSQLt.AssertLike '%el%', 'hello'; - pass +EXEC tSQLt.AssertLike 'h_llo', 'hello'; - pass +EXEC tSQLt.AssertLike '%oo%', 'hello'; - fail + +EXEC tSQLt.AssertLike 'hello', NULL; -- fail +EXEC tSQLt.AssertLike NULL, 'hello'; -- fail +EXEC tSQLt.AssertLike NULL, NULL; -- pass +``` diff --git a/docs/user-guide/assertions/assertnotequals.md b/docs/user-guide/assertions/assertnotequals.md new file mode 100644 index 000000000..3850ddf19 --- /dev/null +++ b/docs/user-guide/assertions/assertnotequals.md @@ -0,0 +1,41 @@ +# AssertNotEquals + +## Syntax + +``` sql +tSQLt.AssertNotEquals [@expected = ] expected value + , [@actual = ] actual value + [, [@message = ] 'message' ] +``` + +## Arguments + +[**@expected** = ] expected value + +The expected value for the test. @expected is SQL_VARIANT with no default. + +[**@actual** = ] actual value + +The actual value resulting from processing during the test. @actual is SQL_VARIANT with no default. + +[**@message** = ] ‘message’ + +Optional. String containing an additional failure message to be used if the expected and actual values are equal. @message is NVARCHAR(MAX) with no default. + +## Return Code Values + +Returns 0 + +## Error Raised + +Raises a `failure` error if expected and actual are equal. + +Raises an `Operand type clash` error if the value passed for @expected or @actual is not compatible with SQL_VARIANT. + +## Result Sets + +None + +## Overview + +AssertNotEquals compares two values for inequality. If they are equal, the test case is failed; otherwise, AssertNotEquals does not affect test processing. For the purposes of AssertNotEquals, NULL is considered equal to NULL. Any non-NULL value is considered not equal to NULL. \ No newline at end of file diff --git a/docs/user-guide/assertions/assertobjectdoesnotexist.md b/docs/user-guide/assertions/assertobjectdoesnotexist.md new file mode 100644 index 000000000..1452d4509 --- /dev/null +++ b/docs/user-guide/assertions/assertobjectdoesnotexist.md @@ -0,0 +1,49 @@ +# AssertObjectDoesNotExist + +## Syntax + +``` sql +tSQLt.AssertObjectDoesNotExist [@objectName = ] 'object name' + [, [@message = ] 'message' ] +``` + +## Arguments + +[**@objectName** = ] ‘object name’ + +The name of a database object which you want to assert non-existence. @objectName is NVARCHAR(MAX) with no default. + +[**@message** = ] ‘message’ + +Optional. String containing an additional failure message to be used if the object does not exist. @message is NVARCHAR(MAX) with no default. + +## Return Code Values +Returns 0 + +## Error Raised +Raises a `failure` error if the specified object does exist. + +## Result Sets +None + +## Overview + +AssertObjectDoesNotExists checks to see that an object with the specified name does not exists in the database. If the name begins with a ‘#’, indicating it is a temporary object (such as a temporary table), then tempdb is checked for the object. + +## Examples + +### Example: Using AssertObjectDoesNotExists to check if an object was dropped + +This test case uses AssertObjectDoesNotExists to test that a stored procedure drops another stored procedure based on the supplied name. + +``` sql +CREATE PROC TestTemplateUtil.[test DropProcedure drops a stored procedure] +AS +BEGIN + EXEC('CREATE PROC dbo.MyProcedure AS RETURN 0;'); + + EXEC TemplateUtil.DropProcedure 'dbo.MyProcedure'; + + EXEC tSQLt.AssertObjectDoesNotExists 'dbo.MyProcedure'; +END; +``` diff --git a/docs/user-guide/assertions/assertobjectexists.md b/docs/user-guide/assertions/assertobjectexists.md new file mode 100644 index 000000000..d550125f2 --- /dev/null +++ b/docs/user-guide/assertions/assertobjectexists.md @@ -0,0 +1,49 @@ +# AssertObjectExists + +## Syntax + +``` sql +tSQLt.AssertObjectExists [@objectName = ] 'object name' + [, [@message = ] 'message' ] +``` + +## Arguments + +[**@objectName** = ] ‘object name’ + +The name of a database object which you want to assert existence. @objectName is NVARCHAR(MAX) with no default. + +[**@message** = ] ‘message’ + +Optional. String containing an additional failure message to be used if the object does not exist. @message is NVARCHAR(MAX) with no default. + +## Return Code Values +Returns 0 + +## Error Raised +Raises a `failure` error if the specified object does not exist. + +## Result Sets +None + +## Overview + +AssertObjectExists checks to see if an object with the specified name exists in the database. If the object name begins with a ‘#’, indicating it is a temporary object (such as a temporary table), then tempdb is searched for the object. + +## Examples + +### Example: Using AssertObjectExists to check if an object was created + +This test case uses AssertObjectExists to test that a stored procedure creates a new stored procedure based on the supplied table name. + +``` sql +CREATE PROC TestTemplateUtil.[test CreateTableTemplate creates an update stored procedure] +AS +BEGIN + CREATE TABLE MyTable (i INT); + + EXEC TemplateUtil.CreateTableTemplate 'MyTable'; + + EXEC tSQLt.AssertObjectExists 'UpdateMyTable'; +END; +``` diff --git a/docs/user-guide/assertions/assertresultsetshavesamemetadata.md b/docs/user-guide/assertions/assertresultsetshavesamemetadata.md new file mode 100644 index 000000000..0e9d3853d --- /dev/null +++ b/docs/user-guide/assertions/assertresultsetshavesamemetadata.md @@ -0,0 +1,49 @@ +# AssertResultSetsHaveSameMetaData + +## Syntax + +``` sql +tSQLt.AssertResultSetsHaveSameMetaData [@expectedCommand = ] 'expected command' + , [@actualCommand = ] 'actual command' +``` + +## Arguments + +[**@expectedCommand** = ] ‘expected command’ + +A command which returns a result set with the expected meta data to compare with. @expectedCommand is NVARCHAR(MAX) with no default. + +[**@actualCommand** = ] ‘actual command’ + +The actual result set whose meta data should be compared with the meta data of the result set from the expected command. @actualCommand is NVARCHAR(MAX) with no default. + +## Return Code Values +Returns 0 + +## Error Raised +Raises a `failure` error if the meta data of the expected command and the meta data of the actual command are not equal. + +## Result Sets +None + +## Overview + +AssertResultSetsHaveSameMetaData executes the expected command and actual command, capturing the result sets from each. The meta data (i.e. the column names and properties) are compared between the two result sets. If they meta data contains differences, then AssertResultSetsHaveSameMetaData fails the test. + +This may be useful, for example, when testing a stored procedure which returns a result set and the names and data types of the columns should be validated. + +## Examples + +## Example: AssertResultSetsHaveSameMetaData to check the meta data properties of a view + +This test case uses AssertResultSetsHaveSameMetaData to check that the meta data of the EmployeeAgeReport view. The view’s meta data is compared against a query provided in the @expectedCommand parameter. + +``` sql +CREATE PROC TestHumanResources.[test EmployeeAgeReport has appropriate meta data] +AS +BEGIN + EXEC tSQLt.AssertResultSetsHaveSameMetaData + 'SELECT CAST(''A'' AS VARCHAR(1000)) AS name, CAST(30 AS SMALLINT) AS age', + 'SELECT name, age FROM HumanResources.EmployeeAgeReport'; +END; +``` diff --git a/docs/user-guide/assertions/fail.md b/docs/user-guide/assertions/fail.md new file mode 100644 index 000000000..71e39e766 --- /dev/null +++ b/docs/user-guide/assertions/fail.md @@ -0,0 +1,71 @@ +# Fail + +## Syntax + +``` sql +tSQLt.Fail [ [@Message0 = ] message part ] + [, [@Message1 = ] message part ] + [, [@Message2 = ] message part ] + [, [@Message3 = ] message part ] + [, [@Message4 = ] message part ] + [, [@Message5 = ] message part ] + [, [@Message6 = ] message part ] + [, [@Message7 = ] message part ] + [, [@Message8 = ] message part ] + [, [@Message9 = ] message part ] +``` + +## Arguments + +[**@Message0 – @Message9** = ] message part + +Optional. The message parts contain the message which will be displayed as the failure message for the test case. Multiple parameters are provided so that the caller does not have to first build up a string variable to pass to the fail procedure. All @Message parameters are NVARCHAR(MAX) with a default of an empty string. + + +## Return Code Values + +Returns 0 + +## Error Raised + +Raises a `failure` error when called, resulting in failure of the test case. + +## Result Sets + +None + +## Overview + +Fail simply fails a test case with the specified failure message. Frequently, use of one of the tSQLt.Assert… procedures is more appropriate. However, there are times when calling Fail is the only option or more convenient. + +## Examples + +### Example: Using Fail to check that a randomly generated number is in the expected range + +This test case repeatedly calls a random number generator. If the generated random number is ever outside of the specified range, the test case is failed using the tSQLt.Fail procedure. This example also demonstrates passing multiple message parameters to Fail: + +``` sql +CREATE PROCEDURE testRandom.[test GetRandomInt(1,10) does not produce values less than 1 or greater than 10] +AS +BEGIN + SET NOCOUNT ON; + + EXEC Random.SeedRandomOnTime; + + DECLARE @numTrials INT; SET @numTrials = 10000; + DECLARE @i INT; SET @i = 0; + DECLARE @r INT; + + WHILE @i < @numTrials + BEGIN + EXEC Random.GetRandomInt 1, 10, @r OUTPUT; + IF @r < 1 OR @r > 10 + BEGIN + EXEC tSQLt.Fail 'Invalid random value returned: ', @r; + END; + + SET @i = @i + 1; + END; +END; +GO +``` diff --git a/docs/user-guide/assertions/index.md b/docs/user-guide/assertions/index.md new file mode 100644 index 000000000..abebffe0a --- /dev/null +++ b/docs/user-guide/assertions/index.md @@ -0,0 +1,15 @@ +# Assertions + +This section contains the various assertion stored procedures which can be used to compare values. Also included is the Fail stored procedure which marks a test case as failed. + +- [AssertEmptyTable](assertemptytable.md) +- [AssertEquals](assertequals.md) +- [AssertEqualsString](assertequalsstring.md) +- [AssertEqualsTable](assertequalstable.md) +- [AssertEqualsTableSchema](assertequalstableschema.md) +- [AssertNotEquals](assertnotequals.md) +- [AssertObjectDoesNotExist](assertobjectdoesnotexist.md) +- [AssertObjectExists](assertobjectexists.md) +- [AssertResultSetsHaveSameMetaData](assertresultsetshavesamemetadata.md) +- [Fail](fail.md) +- [AssertLike](assertlike.md) diff --git a/docs/user-guide/expectations/expectexception.md b/docs/user-guide/expectations/expectexception.md new file mode 100644 index 000000000..3944b4a50 --- /dev/null +++ b/docs/user-guide/expectations/expectexception.md @@ -0,0 +1,80 @@ +# ExpectException +## Syntax + +``` sql +tSQLt.ExpectException + [ [@ExpectedMessage= ] 'expected error message'] + [, [@ExpectedSeverity= ] 'expected error severity'] + [, [@ExpectedState= ] 'expected error state'] + [, [@Message= ] 'supplemental fail message'] + [, [@ExpectedMessagePattern= ] 'expected error message pattern'] + [, [@ExpectedErrorNumber= ] 'expected error number'] +``` + +## Arguments + +[**@ExpectedMessage** = ] ‘expected error message’ + +Optional. The expected error message. The test fails if an error with a different message is thrown. + +[**@ExpectedSeverity** = ] ‘expected error severity’ + +Optional. The expected error severity. The test fails if an error with a different severity is thrown. + +[**@ExpectedState** = ] ‘expected error state’ + +Optional. The expected error state. The test fails if an error with a different state is thrown. + +[**@Message** = ] ‘supplemental fail message’ + +Optional. Supplemental information to clarify the test’s intent. This is displayed in case of a failure. + +[**@ExpectedMessagePattern** = ] ‘expected error message pattern’ + +Optional. A pattern describing the expected error message. The test fails if an error with a message not matching this pattern is thrown. + +[**@ExpectedErrorNumber** = ] ‘expected error number’ + +Optional. The expected error number. The test fails if an error with a different number is thrown. + +## Return code values + +Returns 0 + +## Errors raised +Raises a `failure` error if an error matching the expectation is not raised. + +## Result sets +None + +## Overview + +`tSQLt.ExpectException` marks the point in the test after which an error should be raised. All parameters are optional. Independent of the supplied parameters, the test fails if after the `tSQLt.ExpectException` call no error is raised. Passing in a NULL in any parameter has the same effect as omitting that parameter. + +The parameters allow to constrain the expected exception further. + +There can be only one call to `tSQLt.ExpectException` per test. However, a call to `tSQLt.ExpectException` can follow a call to `tSQLt.ExpectNoException`. + +## Examples +There are two main call patterns: + +``` sql +EXEC tSQLt.ExpectException @ExpectedMessage = 'Some Expected Message', @ExpectedSeverity = NULL, @ExpectedState = NULL; +``` +and + +``` sql +EXEC tSQLt.ExpectException @ExpectedMessagePattern = '%Part of Expected Message%', @ExpectedSeverity = NULL, @ExpectedState = NULL; +``` + +**Example: Using `tSQLt.ExpectException` to check that correct error is raised** + +``` sql +CREATE PROCEDURE PurgeTableTests.[test dbo.PurgeTable rejects not existing table] +AS +BEGIN + EXEC tSQLt.ExpectException @Message = 'Table dbo.DoesNotExist not found.', @ExpectedSeverity = 16, @ExpectedState = 10; + EXEC dbo.PurgeTable @TableName='dbo.DoesNotExist'; +END; +GO +``` diff --git a/docs/user-guide/expectations/expectnoexception.md b/docs/user-guide/expectations/expectnoexception.md new file mode 100644 index 000000000..9b92ea87a --- /dev/null +++ b/docs/user-guide/expectations/expectnoexception.md @@ -0,0 +1,38 @@ +# ExpectNoException + +## Syntax +```sql +tSQLt.ExpectNoException [ [@Message= ] 'supplemental fail message'] +``` + +## Arguments +[**@Message =** ] ‘supplemental fail message’ + +Optional. Supplemental information to clarify the test’s intent. This is displayed in case of a failure. + +## Return Code Values +Returns 0 + +## Errors Raised +Raises a `failure` error if any error is raised after it was called. + +## Result Sets +None + +## Overview +`tSQLt.ExpectNoException` marks the point in the test after which no error should be raised. `tSQLt.ExpectNoException` specifies that the intention of the test is to assert that no error is raised. Therefore the test will fail instead of error, if an error is encountered after `tSQLt.ExpectNoException` was called. + +There can be only one call to `tSQLt.ExpectNoException` per test. However, a call to `tSQLt.ExpectNoException` can be followed by a call to `tSQLt.ExpectException`. + +## Examples +**Example: Using `tSQLt.ExpectNoException` to assert that no error is raised** + +```sql +CREATE PROCEDURE PurgeTableTests.[test dbo.PurgeTableIfExists ignores not existing table] +AS +BEGIN + EXEC tSQLt.ExpectNoException; + EXEC dbo.PurgeTableIfExists @TableName='dbo.DoesNotExist'; +END; +GO +``` \ No newline at end of file diff --git a/docs/user-guide/expectations/index.md b/docs/user-guide/expectations/index.md new file mode 100644 index 000000000..6ea22ea4d --- /dev/null +++ b/docs/user-guide/expectations/index.md @@ -0,0 +1,5 @@ +# Expectations +This section contains the various expectation procedures which can be used to indicate code behavior expectations. + +- [ExpectException](expectexception.md) +- [ExpectNoException](expectnoexception.md) \ No newline at end of file diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md new file mode 100644 index 000000000..de3b85ec7 --- /dev/null +++ b/docs/user-guide/index.md @@ -0,0 +1,42 @@ +--- +--- +# User Guide + +This reference contains an explanation of each of the public tables, views, stored procedures and functions provided by tSQLt. + +## [Test creation and execution](test-creation-and-execution) + +- [NewTestClass](test-creation-and-execution/newtestclass) +- [DropClass](test-creation-and-execution/dropclass) +- [RunAll](test-creation-and-execution/runall) +- [Run](test-creation-and-execution/run) +- [RenameClass](test-creation-and-execution/renameclass) + +## [Assertions](assertions) + +- [AssertEmptyTable](assertions/assertemptytable) +- [AssertEquals](assertions/assertequals) +- [AssertEqualsString](assertions/assertequalsstring) +- [AssertEqualsTable](assertions/assertequalstable) +- [AssertEqualsTableSchema](assertions/assertequalstableschema) +- [AssertNotEquals](assertions/assertnotequals) +- [AssertObjectDoesNotExist](assertions/assertobjectdoesnotexist) +- [AssertObjectExists](assertions/assertobjectexists) +- [AssertResultSetsHaveSameMetaData](assertions/assertresultsetshavesamemetadata) +- [Fail](assertions/fail) +- [AssertLike](assertions/assertlike) + +## [Expectations](expectations) + +- [ExpectException](expectations/expectexception) +- [ExpectNoException](expectations/expectnoexception) + +## [Isolating dependencies](isolating-dependencies) + +- [ApplyConstraint](isolating-dependencies/applyconstraint) +- [FakeFunction](isolating-dependencies/fakefunction) +- [FakeTable](isolating-dependencies/faketable) +- [RemoveObjectIfExists](isolating-dependencies/removeobjectifexists) +- [SpyProcedure](isolating-dependencies/spyprocedure) +- [ApplyTrigger](isolating-dependencies/applytrigger) +- [RemoveObject](isolating-dependencies/removeobject) diff --git a/docs/user-guide/isolating-dependencies/applyconstraint.md b/docs/user-guide/isolating-dependencies/applyconstraint.md new file mode 100644 index 000000000..e349c43d2 --- /dev/null +++ b/docs/user-guide/isolating-dependencies/applyconstraint.md @@ -0,0 +1,108 @@ +# ApplyConstraint +## Syntax + +``` sql +tSQLt.ApplyConstraint [@TableName = ] 'table name' + , [@ConstraintName = ] 'constraint name' + , [@SchemaName = ] 'schema name' +``` + +## Arguments + +[**@TableName** = ] ‘table name’ + +The name of the table where the constraint should be applied. Should contain both the schema name and the table name. + +[**@ConstraintName** = ] ‘constraint name’ + +The name of the constraint to be applied. Should not include the schema name or table name. + +[**@SchemaName** = ] ‘schema name’ – **Deprecated: do not use, will be removed in future version** + +## Return code values +Returns 0 + +## Error raised + +If the specified table or constraint does not exist an error is thrown: ApplyConstraint could not resolve the object names, ‘%s’, ‘%s’. + +## Result sets +None + +## Overview +We want to be able to test constraints individually. We can use FakeTable to remove all the constraints on a table, and ApplyConstraint to add back in the one which we want to test. + +ApplyConstraint in combination with FakeTable allows constraints to be tested in isolation of other constraints on a table. + +## Limitations +ApplyConstraint works with the following constraint types: + +- CHECK constraints +- FOREIGN KEY constraints +- UNIQUE constraints +- PRIMARY KEY constraints + +There are the following limitations: + +- Cascade properties of FOREIGN KEY constraints are not preserved. +- SQL Server automatically creates unique indexes for UNIQUE and PRIMARY KEY constraints. Those indexes for “applied” constraints do not preserve asc/desc properties of the original supporting indexes. + +Note: Applying a PRIMARY KEY constraint will automatically change the involved columns of the faked table to be “NOT NULL”able. + +## Examples +**Example: Using ApplyConstraint to test a Foreign Key Constraint** + +In this example, we have a foreign key constraint on the ReferencingTable. We would like to test this constraint and have two test cases. + +The first test ensures that the foreign key prevents inappropriate inserts. It does this by faking the two tables involved and then calling ApplyConstraint. Then an attempt is made to insert a record into the ReferencingTable with no record in the ReferencedTable. The exception is caught and the test passes or fails based on this exception. + +The second test makes sure that appropriate records can be inserted. Again, the two tables are faked and ApplyConstraint is called. If any exception is thrown when we attempt to insert a record into ReferencingTable, the test will fail (because any uncaught exception will cause a test to fail). + +``` sql +EXEC tSQLt.NewTestClass 'ConstraintTests'; +GO + +CREATE PROCEDURE ConstraintTests.[test ReferencingTable_ReferencedTable_FK prevents insert of orphaned rows] +AS +BEGIN + EXEC tSQLt.FakeTable 'dbo.ReferencedTable'; + EXEC tSQLt.FakeTable 'dbo.ReferencingTable'; + + EXEC tSQLt.ApplyConstraint 'dbo.ReferencingTable','ReferencingTable_ReferencedTable_FK'; + + DECLARE @ErrorMessage NVARCHAR(MAX); SET @ErrorMessage = ''; + + BEGIN TRY + INSERT INTO dbo.ReferencingTable + ( id, ReferencedTableId ) + VALUES ( 1, 11 ) ; + END TRY + BEGIN CATCH + SET @ErrorMessage = ERROR_MESSAGE(); + END CATCH + + IF @ErrorMessage NOT LIKE '%ReferencingTable_ReferencedTable_FK%' + BEGIN + EXEC tSQLt.Fail 'Expected error message containing ''ReferencingTable_ReferencedTable_FK'' but got: ''',@ErrorMessage,'''!'; + END + +END +GO + +CREATE PROCEDURE ConstraintTests.[test ReferencingTable_ReferencedTable_FK allows insert of non-orphaned rows] +AS +BEGIN + EXEC tSQLt.FakeTable 'dbo.ReferencedTable'; + EXEC tSQLt.FakeTable 'dbo.ReferencingTable'; + + EXEC tSQLt.ApplyConstraint 'dbo.ReferencingTable','ReferencingTable_ReferencedTable_FK'; + + INSERT INTO dbo.ReferencedTable + ( id ) + VALUES ( 11 ) ; + INSERT INTO dbo.ReferencingTable + ( id, ReferencedTableId ) + VALUES ( 1, 11 ) ; +END +GO +``` \ No newline at end of file diff --git a/docs/user-guide/isolating-dependencies/applytrigger.md b/docs/user-guide/isolating-dependencies/applytrigger.md new file mode 100644 index 000000000..a3a2aa3a7 --- /dev/null +++ b/docs/user-guide/isolating-dependencies/applytrigger.md @@ -0,0 +1,64 @@ +# ApplyTrigger +## Syntax + +``` sql +tSQLt.ApplyTrigger [@TableName = ] 'table name' + , [@TriggerName = ] 'trigger name' +``` + +## Arguments + +[**@TableName** = ] ‘table name’ +The name of the table where the constraint should be applied. Should contain both the schema name and the table name. + +[**@TriggerName** = ] ‘trigger name’ + +The name of the trigger to be applied. Should not include the schema name or table name. + +## Return code values +Returns 0 + +## Error raised +If the specified table or trigger does not exist an error is thrown. + +## Result sets +None + +## Overview +We want to be able to test triggers individually. We can use FakeTable to remove all the constraints and triggers from a table, and ApplyTrigger to add back in the one which we want to test. + +ApplyTrigger in combination with FakeTable allows triggers to be tested in isolation of constraints and other triggers on a table. + +## Examples +**Example: Using ApplyTrigger to test a trigger** + +In this example, the test isolates the AuditInserts trigger from other constraints and triggers on the Registry.Student table. This allows us to test that the trigger inserts a record into the Logs.Audit table when a new student is inserted into the Student table. + +``` sql +EXEC tSQLt.NewTestClass 'AuditTests'; +GO + +CREATE PROCEDURE AuditTests.[test inserting record into Student table creates Audit record] +AS +BEGIN + EXEC tSQLt.FakeTable 'Registry.Student'; + EXEC tSQLt.FakeTable @TableName = 'Logs.Audit'; + EXEC tSQLt.ApplyTrigger 'Registry.Student', 'AuditInserts'; + + INSERT INTO Registry.Student (StudentId) VALUES (1); + + SELECT LogMessage + INTO #Actual + FROM Logs.Audit; + + SELECT TOP(0) * + INTO #Expected + FROM #Actual; + + INSERT INTO #Expected + VALUES('Student record created, id = 1'); + + EXEC tSQLt.AssertEqualsTable '#Expected','#Actual'; +END; +GO +``` \ No newline at end of file diff --git a/docs/user-guide/isolating-dependencies/fakefunction.md b/docs/user-guide/isolating-dependencies/fakefunction.md new file mode 100644 index 000000000..679b87fc0 --- /dev/null +++ b/docs/user-guide/isolating-dependencies/fakefunction.md @@ -0,0 +1,92 @@ +# FakeFunction + +## Syntax + +``` sql +tSQLt.FakeFunction [@FunctionName = ] 'function name' + , [@FakeFunctionName = ] 'fake function name' +``` + +## Arguments + +[**@FunctionName** = ] ‘function name’ + +The name of an existing function. @FunctionName is NVARCHAR(MAX) with no default. @FunctionName should include the schema name of the function. For example: MySchema.MyFunction + +[**@FakeFunctionName** = ] ‘fake function name’ + +The name of an existing function that will replace the function defined by @FunctionName during the test. @FakeFunctionName is NVARCHAR(MAX) with no default. + +## Return code values +Returns 0 + +## Error raised + +If the function itself or the fake function does not exist, the follow error is raised: ‘function name’ does not exist + +If the function and the fake function are not compatible function types (i.e. they must both be scalar functions or both be table valued functions), the following error is raised: Both parameters must contain the name of either scalar or table valued functions! + +If the parameters of the function and fake function are not the same, the following error is raised: Parameters of both functions must match! (This includes the return type for scalar functions.) + +## Result sets +None + +## Overview +Code that calls a function can be difficult to test if that function performs significant logic. We want to isolate the code we are testing from the logic buried in the functions that it calls. To create independent tests, we can replace a called function with a fake function. The fake function will perform much simpler logic that supports the purpose of our test. Often, the fake function will simply return a hard-coded value. + +Alternatively, the fake function may ‘validate’ the parameters it receives by returning one value if the parameters match expectations, and another value if the parameters do not match expectations. That way the code that calls the function will have a different result and thus the parameter passed to the function can be tested. + +## Warnings +Remember that if you are faking a function, you are not testing that function. Your test is trying to test something else: typically, the logic of a view, stored procedure or another function that interacts with the function you are faking. + +## Examples +**Example: Using FakeFunction to avoid executing the logic of a complex function** + +In this example, we want to test a sales report view, SalesReport. The sales report view will return the EmployeeId, RevenueFromSales (the total amount of new revenue the employee generated) and their Commission. (The commision has to be calculated with a complex algorithm using RevenueFromSales and values read from the EmployeeCompensation table. This computation is done by the ComputeCommision scalar function.) + +Since we are testing the SalesReport view, we will fake the ComputeCommission function. + +``` sql +EXEC tSQLt.NewTestClass 'SalesAppTests'; +GO + +CREATE FUNCTION SalesAppTests.Fake_ComputeCommission ( + @EmployeeId INT, + @RevenueFromSales DECIMAL(10,4) +) +RETURNS DECIMAL(10,4) +AS +BEGIN + RETURN 1234.5678; +END; +GO + +CREATE PROCEDURE SalesAppTests.[test SalesReport returns revenue and commission] +AS +BEGIN +-------Assemble + EXEC tSQLt.FakeFunction 'SalesApp.ComputeCommission', 'SalesAppTests.Fake_ComputeCommission'; + EXEC tSQLt.FakeTable 'SalesApp.Employee'; + EXEC tSQLT.FakeTable 'SalesApp.Sales'; + + INSERT INTO SalesApp.Employee (EmployeeId) VALUES (1); + INSERT INTO SalesApp.Sales (EmployeeId, SaleAmount) VALUES (1, 10.1); + INSERT INTO SalesApp.Sales (EmployeeId, SaleAmount) VALUES (1, 20.2); + +-------Act + SELECT EmployeeId, RevenueFromSales, Commission + INTO SalesAppTests.Actual + FROM SalesApp.SalesReport; + +-------Assert + SELECT TOP(0) * + INTO SalesAppTests.Expected + FROM SalesAppTests.Actual; + + INSERT INTO SalesAppTests.Expected (EmployeeId, RevenueFromSales, Commission) + VALUES (1, 30.3, 1234.5678); + + EXEC tSQLt.AssertEqualsTable 'SalesAppTests.Expected', 'SalesAppTests.Actual'; +END; +GO +``` \ No newline at end of file diff --git a/docs/user-guide/isolating-dependencies/faketable.md b/docs/user-guide/isolating-dependencies/faketable.md new file mode 100644 index 000000000..056809bf4 --- /dev/null +++ b/docs/user-guide/isolating-dependencies/faketable.md @@ -0,0 +1,91 @@ +# FakeTable + +## Syntax + +```sql +tSQLt.FakeTable [@TableName = ] 'table name' + , [[@SchemaName = ] 'schema name'] + , [[@Identity = ] 'preserve identity'] + , [[@ComputedColumns = ] 'preserve computed columns'] + , [[@Defaults = ] 'preserve default constraints'] +``` + +## Arguments + +[**@TableName =** ] ‘table name’ + +The name of the table for which to create a fake table. Should contain both the schema name and the table name. + +[**@SchemaName =** ] ‘schema name’ – **Deprecated: do not use, will be removed in future version** + +[**@Identity =** ] ‘preserve identity’ – Indicates if the identity properties of an identity column should be preserved. If @Identity = 1, the identity properties will be preserved, otherwise the faked table will have the identity properties removed. The default is @Identity = 0. + +[**@ComputedColumns =** ] ‘preserve computed columns’ – Indicates if the computations on computed columns should be preserved. If @ComputedColumns = 1, the computations will be preserved, otherwise the faked table will have computations removed from computed columns. The default is @ComputedColumns = 0. + +[**@Defaults =** ] ‘preserve default constraints’ – Indicates if the default constraints on columns should be preserved. If @Defaults = 1, default constraints will be preserved for all columns on the faked table, otherwise the faked table will not contain default constraints. The default is @Defaults = 0. + +## Return Code Values + +Returns 0 + +## Error Raised + +If the specified table does not exist an error is thrown: FakeTable could not resolve the object name, ‘%s’. + +## Result Sets + +None + +## Overview + +We want to keep our test cases focused and do not want to insert data which is irrelevant to the current test. However, table and column constraints can make this difficult. + +FakeTable allows tests to be written in isolation of the constraints on a table. FakeTable creates an empty version of the table without the constraints in place of the specified table. Therefore any statements which access the table during the execution of the test case are actually working against the fake table with no constraints. When the test case completes, the original table is put back in place because of the rollback which tSQLt performs at the end of each test case. + +FakeTable can be called on Tables, Views and Synonyms (A Synonym has to point to a table or view in the current database.) + +## Limitations + +FakeTable cannot be used with temporary tables (tables whose names begin with # or ##). + +## Warnings + +Remember if you are faking a table, you are not testing the constraints on the table. You will want test cases that also exercise the constraints on the table. See Also: ApplyConstraint. + +## Examples + +**Example: Faking a table to create test data to test a view** + +In this example, we have a FinancialApp.ConvertCurrencyUsingLookup view which accesses the FinancialApp.CurrencyConversion table. In order to test the view, we want to freely insert data into the table. FakeTable is used to remove the constraints from the CurrencyConversion table so that the data record can be inserted. + +```sql +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 +``` + +**Example: Calling FakeTable with @Identity = 1** + +We need to specify the @Identity parameter explicitly because the second parameter (@SchemaName) has been deprecated. This same pattern applies to @ComputedColumns and @Defaults. + +```sql +EXEC tSQLt.FakeTable 'FinancialApp.CurrencyConversion', @Identity = 1; +``` \ No newline at end of file diff --git a/docs/user-guide/isolating-dependencies/index.md b/docs/user-guide/isolating-dependencies/index.md new file mode 100644 index 000000000..df60d0730 --- /dev/null +++ b/docs/user-guide/isolating-dependencies/index.md @@ -0,0 +1,11 @@ +# Isolating dependencies + +This group of objects enables you to isolate the are of code which you are testing. When you are unit testing a complex system, it is desirable to isolate certain parts to test. For example, if you have a complicated set of tables which are related through foreign keys, it can be very difficult to insert test data into one of the tables. The objects in this section provide the ability to focus on a single unit to test. + +- [ApplyConstraint](applyconstraint.md) +- [ApplyTrigger](applytrigger.md) +- [FakeFunction](fakefunction.md) +- [FakeTable](faketable.md) +- [RemoveObject](removeobject.md) +- [RemoveObjectIfExists](removeobjectifexists.md) +- [SpyProcedure](spyprocedure.md) \ No newline at end of file diff --git a/docs/user-guide/isolating-dependencies/removeobject.md b/docs/user-guide/isolating-dependencies/removeobject.md new file mode 100644 index 000000000..b78e09bfb --- /dev/null +++ b/docs/user-guide/isolating-dependencies/removeobject.md @@ -0,0 +1,74 @@ +# RemoveObject + +## Syntax + +```sql +tSQLt.RemoveObject [@ObjectName= ] 'object name' + [, [@NewName = ] 'new object name' OUTPUT] + [, [@IfExists = ] ( 0 | 1 )] +``` + +## Arguments + +[**@ObjectName =** ] ‘object name’ + +The name of the object to be renamed. The name should include both the schema and object name. + +[**@NewName =** ] ‘new object name’ OUTPUT + +The automatically generated name the object was given after being renamed. This is an output parameter only. If a value is provided here as input, it will not be used. + +[**@IfExists =** ] ( 0 | 1 ) + +If @IfExists = 0 is passed, an error is raised if the object does not exist. @IfExists = 1 causes tSQLt.RemoveObject to be a NoOp if the object does not exist. The default is 0. + +## Return Code Values + +Returns 0 + +## Error Raised + +If the specified object does not exist an error is thrown: %s does not exist. + +## Result Sets + +None + +## Overview +Often times an object needs to be replaced with a mock. The first step in replacing an object with a mock is to remove the original object. tSQLt.RemoveObject removes the original object by renaming it. The new name for the object is automatically generated to avoid collisions with other objects. + +For tables and stored procedures, please refer to [FakeTable](faketable.md) and [SpyProcedure](spyprocedure.md), respectively. + +## Examples + +**Example: Replacing a function with a stub** + +In this example, we are testing that GetRecentUsers view returns only users from the past 10 minutes. In this example, we have a util.GetCurrentDate function which returns the current date and is used by GetRecentUsers. For the purposes of our test, we want to return a constant date. RemoveObject is called so that we can create a stub function which returns a hard-coded value. + +```sql +CREATE PROCEDURE GetRecentUsersTests.[test that GetRecentUsers returns users from the past 10 minutes] +AS +BEGIN + EXEC tSQLt.FakeTable 'dbo.Users'; + INSERT INTO dbo.Users (username, startdate) VALUES ('bob', '2013-03-15 11:59:59'); + INSERT INTO dbo.Users (username, startdate) VALUES ('joe', '2013-03-15 12:00:00'); + INSERT INTO dbo.Users (username, startdate) VALUES ('sue', '2013-03-15 12:01:00'); + + EXEC tSQLt.RemoveObject 'util.GetCurrentDate'; + EXEC ('CREATE FUNCTION util.GetCurrentDate() RETURNS datetime AS BEGIN RETURN ''2013-03-15 12:10:00''; END;'); + + SELECT username, startdate + INTO #Actual + FROM dbo.GetRecentUsers; + + SELECT TOP(0) * + INTO #Expected + FROM #Actual; + + INSERT INTO #Expected (username, startdate) VALUES ('joe', '2013-03-15 12:00:00'); + INSERT INTO #Expected (username, startdate) VALUES ('sue', '2013-03-15 12:01:00'); + + EXEC tSQLt.AssertEqualsTable '#Expected', '#Actual'; +END; +GO +``` \ No newline at end of file diff --git a/docs/user-guide/isolating-dependencies/removeobjectifexists.md b/docs/user-guide/isolating-dependencies/removeobjectifexists.md new file mode 100644 index 000000000..41333cb5c --- /dev/null +++ b/docs/user-guide/isolating-dependencies/removeobjectifexists.md @@ -0,0 +1,21 @@ +# RemoveObjectIfExists + +## Syntax + +```sql +tSQLt.RemoveObjectIfExists [@ObjectName= ] 'object name' + [, [@NewName = ] 'new object name' OUTPUT] +``` + +## Arguments + +[**@ObjectName =** ] ‘object name’ + +The name of the object to be renamed. The name should include both the schema and object name. + +[**@NewName =** ] ‘new object name’ OUTPUT + +The automatically generated name the object was given after being renamed. This is an output parameter only. If a value is provided here as input, it will not be used. + +## Overview +`tSQLt.RemoveObjectIfExists` is a short form of `tSQLt.RemoveObject` with @IfExist = 1. See there for details. \ No newline at end of file diff --git a/docs/user-guide/isolating-dependencies/spyprocedure.md b/docs/user-guide/isolating-dependencies/spyprocedure.md new file mode 100644 index 000000000..86ed4c24a --- /dev/null +++ b/docs/user-guide/isolating-dependencies/spyprocedure.md @@ -0,0 +1,122 @@ +# SpyProcedure + +## Syntax + +```sql +tSQLt.SpyProcedure [@ProcedureName = ] 'procedure name' + [, [@CommandToExecute = ] 'command' ] +``` + +## Arguments +[**@ProcedureName =** ] ‘procedure name’ + +The name of an existing stored procedure. @ProcedureName is NVARCHAR(MAX) with no default. @ProcedureName should include the schema name of the stored procedure. For example: +MySchema.MyProcedure + +[**@CommandToExecute =** ] ‘command’ + +An optional command to execute when a call to Procedure Name is made. @CommandToExecute is NVARCHAR(MAX) with no default. + +## Return Code Values + +Returns 0 + +## Error Raised + +If the procedure contains more than 1020 parameters, SpyProcedure cannot be used and the following error is raised: Cannot use SpyProcedure on procedure ‘procedure name’ because it contains more than 1020 parameters. + +If the object given by procedure name does not exist or is not a stored procedure, the follow error is raised: Cannot use SpyProcedure on ‘procedure name’ because the procedure does not exist. + +## Result Sets + +None + +## Overview + +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 independently 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. + +SpyProcedure allows tests to be written for a procedure in isolation of the other procedures that it calls. SpyProcedure creates a table with the name of @ProcedureName + ‘_SpyProcedureLog’. This table contains an identity column ‘_id_’ and a column for each procedure parameter (except for cursor output parameters). SpyProcedure also replaces the procedure named by @ProcedureName with the command provided in the @CommandToExecute parameter and a command to insert the parameter values into the SpyProcedureLog table. + +Therefore, whenever the @ProcedureName is executed during the test instead of actually running the procedure, a new log entry is made in the @ProcedureName_SpyProcedureLog table and @CommandToExecute is called. + +## Limitations +SpyProcedure can not be used with temporary stored procedures (stored procedures whose name begins with #). + +SpyProcedure can not be used with procedures which have more than 1020 columns. + +## Warnings + +Remember that if you are spying a procedure, you are not testing that procedure. Your test is trying to test something else: typically, another procedure’s interaction with the procedure you are spying. + +## Examples + +**Example: Using SpyProcedure to record parameters passed to a procedure** + +In this example, we have a sales report which will 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. + +```sql +CREATE PROCEDURE testFinancialApp.[test that SalesReport calls HistoricalReport 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 'actual', 'expected'; + +-------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: Using SpyProcedure to return a hard-coded set of output parameter values** + +Suppose we want to test the procedure, IsDiskSpaceTooLow, which returns a 0 if there is enough disk space, and -1 if there is not enough disk space. IsDiskSpaceTooLow conveniently calls another procedure, GetDiskSpace which returns an output parameter for the current disk space. Since setting up a test to fill the drive to a certain size is probably a bad idea, we can test IsDiskSpaceTooLow by using SpyProcedure on GetDiskSpace and hard-coding the output parameter for the purposes of the test. + +```sql +CREATE PROCEDURE DiskUtil.GetDiskSpace @DiskSpace INT OUT +AS +BEGIN + -- This procedure does something to return the disk space as @DiskSpace output parameter +END +GO + +CREATE PROCEDURE DiskUtil.IsDriveSpaceTooLow +AS +BEGIN + DECLARE @DiskSpace INT; + EXEC DiskUtil.GetDiskSpace @DiskSpace = @DiskSpace OUT; + + IF @DiskSpace < 512 + RETURN -1; + ELSE + RETURN 0; +END; +GO + +CREATE PROCEDURE testDiskUtil.[test IsDriveSpaceTooLow returns -1 if drive space is less than 512 MB] +AS +BEGIN + EXEC tSQLt.SpyProcedure 'DiskUtil.GetDiskSpace', 'SET @DiskSpace = 511'; + + DECLARE @ReturnValue INT; + EXEC @ReturnValue = DiskUtil.IsDriveSpaceTooLow; + + EXEC tSQLt.AssertEquals -1, @ReturnValue; +END +GO +``` \ No newline at end of file diff --git a/docs/user-guide/test-creation-and-execution/dropclass.md b/docs/user-guide/test-creation-and-execution/dropclass.md new file mode 100644 index 000000000..55b3f3e32 --- /dev/null +++ b/docs/user-guide/test-creation-and-execution/dropclass.md @@ -0,0 +1,37 @@ +# DropClass + +## Syntax +`tSQLt.DropClass [@ClassName = ] 'class name'` + +## Arguments +[@ClassName = ] ‘class name’ +The name of the class (schema) to be dropped + +## Return Code Values +Returns 0 + +## Error Raised +An error may be raised if the test class already exists and an object belonging to it cannot be dropped. + +## Result Sets +None + +## Overview +tSQLt.DropClass drops a schema and all objects belonging to it. If the schema does not exist, nothing happens. + +## Limitations +N/A + +## Warnings +Care should be used when executing tSQLt.DropClass as it drops a schema and all objects on that schema. + +## Examples +Example: Creating a test class and the dropping it + +``` +EXEC tSQLt.NewTestClass 'testFinancialApp'; +GO + +EXEC tSQLt.DropClass 'testFinancialApp'; +GO +``` \ No newline at end of file diff --git a/docs/user-guide/test-creation-and-execution/index.md b/docs/user-guide/test-creation-and-execution/index.md new file mode 100644 index 000000000..eda75e8ff --- /dev/null +++ b/docs/user-guide/test-creation-and-execution/index.md @@ -0,0 +1,26 @@ +# Test Creation and Execution + +This section describes how test cases are created and executed. + +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. 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. +1. For each of the test cases: + 1. A record is created indicating that the test case is being executed in the tSQLt.TestResult table. + 1. tSQLt starts a transaction. + 1. If there is a stored procedure named SetUp on the test class, it is executed. + 1. The test case stored procedure is executed. + 1. The transaction is rolled-back. + 1. The record in tSQLt.TestResult is updated accordingly if the test case succeeded, failed or threw an error. +1. 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. \ No newline at end of file diff --git a/docs/user-guide/test-creation-and-execution/newtestclass.md b/docs/user-guide/test-creation-and-execution/newtestclass.md new file mode 100644 index 000000000..6540a245a --- /dev/null +++ b/docs/user-guide/test-creation-and-execution/newtestclass.md @@ -0,0 +1,63 @@ +# NewTestClass + +## Syntax +`tSQLt.NewTestClass [@ClassName = ] 'class name'` + +## Arguments +`[@ClassName = ] ‘class name’` +The name of the test class to be created + +## Return Code Values +Returns 0 + +## Error Raised +An error may be raised if the test class already exists and an object belonging to it cannot be dropped. + +## Result Sets +None + +## Overview +tSQLt.NewTestClass creates a new test class. A test class is simply a schema where the user can create test case procedure and any other related objects. If a schema with the same name as the test class already exists, the schema is dropped first. + +Stored procedures in the test class schema are considered test cases if their name begins with ‘test’. + +## Limitations +N/A + +## Warnings +All test case stored procedures should be created in a schema which was created with tSQLt.NewTestClass. + +## Examples +Example: Creating a test class and test case + +``` +EXEC tSQLt.NewTestClass 'testFinancialApp'; +GO + +CREATE PROCEDURE testFinancialApp.[test that SalesReport calls HistoricalReport 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') AS ex(currency); + + EXEC tSQLt.assertEqualsTable 'actual', 'expected'; + +-------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 +``` diff --git a/docs/user-guide/test-creation-and-execution/renameclass.md b/docs/user-guide/test-creation-and-execution/renameclass.md new file mode 100644 index 000000000..8afd588e1 --- /dev/null +++ b/docs/user-guide/test-creation-and-execution/renameclass.md @@ -0,0 +1,40 @@ +# RenameClass + +## Syntax +`tSQLt.RenameClass [@SchemaName = ] 'class name' + , [@NewSchemaName = ] 'new class name'` + +## Arguments +`[@SchemaName = ] ‘class name’ +The name of the class (schema) to be renamed +[@NewSchemaName = ] ‘new class name’ +The new name of the test class (schema)` + +## Return Code Values +Returns 0 + +## Error Raised +An error may be raised if an object on the test class cannot be transferred to the new test class. + +## Result Sets +None + +## Overview + +tSQLt.RenameClass creates a new test class schema. All objects from the original test class schema are tranfered to the new test class schema. The original test class schema is then dropped. + +## Limitations +N/A + +## Warnings +N/A + +## Examples + +Example: Creating and then renaming a test class + +```EXEC tSQLt.NewTestClass 'testFinancialApp'; +GO + +EXEC tSQLt.RenameClass 'testFinancialApp', 'FinancialAppTests'; +GO``` diff --git a/docs/user-guide/test-creation-and-execution/run.md b/docs/user-guide/test-creation-and-execution/run.md new file mode 100644 index 000000000..9b6f19827 --- /dev/null +++ b/docs/user-guide/test-creation-and-execution/run.md @@ -0,0 +1,57 @@ +# Run + +## Syntax +`tSQLt.Run [ [@testName = ] 'test name', [@TestResultFormatter =] 'test result formatter']` + +## Arguments +`[@testName = ] ‘test name’` + +Optional. The name of a test case, including the schema name to which the test case belongs. For example ‘MyTestClass.[test employee has manager]’. If not provided, the test name provided on the previous call to tSQLt.Run within the current session is used. If no test cases have been run previously in the current session, then no test cases are executed. Optionally, ‘test name’ may be the name of a test class. In which case all tests on that class are executed. + +`[@TestResultFormatter = ] ‘test result formatter’` + +Optional. The name of the stored procedure(accessible to tSQLt) to format the test results. DefaultTestFormatter will be used if this value is not provided. + +## Return Code Values +Returns 0 + +## Error Raised +Raises an error containing the test case statistics if any test fails or errors. For example, if a test case fails, the following error is raised: + +``` +Msg 50000, Level 16, State 10, Line 1 +Test Case Summary: 117 test case(s) executed, 116 succeeded, 1 failed, 0 errored. +``` + +## Result Sets +None + +## Overview + +(Note that tSQLt requires test procedure names to start with the four letters test.) +tSQLt.Run is a flexible procedure allowing three different ways of executing test cases: + +1. Providing a test class name executes all tests in that test class. If a SetUp stored procedure exists in that test class, then it is executed before each test. +2. Providing a test case name executes that single test. +3. Providing no parameter value executes tSQLt.Run the same way the previous call to tSQLt.Run was made when a parameter was provided. This essentially caches the parameter value of tSQLt.Run so that it does not need to be retyped each time. + +tSQLt.Run displays a test case summary. By default, the test case summary is a text based table. However, the result format can be changed using the stored procedure, SetTestResultFormatter. + +## Limitations +N/A + +## Warnings +N/A + +## Examples + +``` +-- Runs all the tests on MyTestClass +EXEC tSQLt.Run 'MyTestClass'; + +-- Runs [MyTestClass].[test addNumbers computes 2 plus 2 equals 4] and executes the SetUp procedure +EXEC tSQLt.Run 'MyTestClass.[test addNumbers computes 2 plus 2 equals 4]'; + +-- Runs using the parameter provided last time tSQLt.Run was executed +EXEC tSQLt.Run; +``` diff --git a/docs/user-guide/test-creation-and-execution/runall.md b/docs/user-guide/test-creation-and-execution/runall.md new file mode 100644 index 000000000..6a3306ef3 --- /dev/null +++ b/docs/user-guide/test-creation-and-execution/runall.md @@ -0,0 +1,29 @@ +# RunAll + +## Syntax +`tSQLt.RunAll` + +## Arguments +none + +## Return Code Values +Returns 0 + +## Error Raised +Raises an error containing the test case statistics if any test fails or errors. For example, if a test case fails, the following error is raised: +``` +Msg 50000, Level 16, State 10, Line 1 +Test Case Summary: 117 test case(s) executed, 116 succeeded, 1 failed, 0 errored. +``` + +## Result Sets +None + +## Overview +tSQLt.RunAll executes all tests in all test classes created with tSQLt.NewTestClass in the current database. If the test class schema contains a stored procedure called SetUp, it is executed before calling each test case. The name of each test case stored procedure must begin with ‘test’. RunAll displays a test case summary. By default, the test case summary is a text based table. However, the result format can be changed using the stored procedure, SetTestResultFormatter. + +## Limitations +N/A + +## Warnings +N/A \ No newline at end of file diff --git a/tSQLt b/tSQLt deleted file mode 160000 index 8a3639d8c..000000000 --- a/tSQLt +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8a3639d8cc53ca5560a49c9a6baa4a5ff7dbb39a