{"id":290,"date":"2007-07-31T00:00:00","date_gmt":"2007-07-30T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/close-these-loopholes-in-your-database-testing\/"},"modified":"2021-09-29T16:22:16","modified_gmt":"2021-09-29T16:22:16","slug":"close-these-loopholes-in-your-database-testing","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/close-these-loopholes-in-your-database-testing\/","title":{"rendered":"Close These Loopholes in Your Database Testing"},"content":{"rendered":"<div class=\"spaced-bottom padded--tight scheme--lightest-grey\">\n<img decoding=\"async\" class=\"media__image\" style=\"float: left; vertical-align: middle;\" src=\"https:\/\/assets.red-gate.com\/external\/SimpleTalk\/database-devops-cog-92.png\" \/> <\/p>\n<p class=\"padded-top--tight\"><strong>DevOps, Continuous Delivery & Database Lifecycle Management<\/strong><br \/>Continuous Integration<\/p>\n<\/div>\n\n<p>Incorrect or incomplete database unit tests may run successfully, but they may not expose all the problems in your application &#8211; they can only give you a false sense of security. The following guidelines will help you improve your database unit tests. <\/p>\n<h3>Have settings on your Test Server match the Production Server.<\/h3>\n<p>Clearly this is an extremely trivial recommendation. However, I cannot emphasize enough how important it is. Discrepancies in database settings may cause your application to silently behave differently, without raising any errors &#8211; such problems may be very difficult to detect. For instance, the following code snippet will run differently against databases with different default collations:<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">CREATE TABLE t(a CHAR(1), b CHAR(1))<\/p>\n<p class=\"MsoNormal\">INSERT t(a, b) VALUES(&#8216;a&#8217;, &#8216;A&#8217;)<\/p>\n<p class=\"MsoNormal\">SELECT COUNT(*) FROM t WHERE a=b<\/p>\n<p>You can create the following two databases, run the code snippet against both, and see for yourself that it returns 1 on your case insensitive database, and 0 on your case sensitive one:<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">CREATE DATABASE [TestCI] COLLATE SQL_Latin1_General_CP1_CI_AS<br \/>\nGO<br \/>\nCREATE DATABASE [TestCS] COLLATE SQL_Latin1_General_CP1_CS_AS<br \/>\nGO<\/p>\n<p>There are many other settings that may affect your application, such as default isolation level (<b>READ COMMITTED<\/b> vs. <b>READ COMMITTED SNAPSHOT<\/b>), and settings on nested and\/or recursive triggers. Providing a complete list of settings and explaining how they may affect your application is beyond the scope of this article. For an example on how snapshot isolation may affect your modifications, refer to a series of Hugo Kornelis&#8217;s posts named &#8221; Snapshot isolation: A threat for integrity?&#8221; and published on sqlblog.com. I describe how settings on nested and\/or recursive triggers have broken an application in my article &#8221; Avoid These Common Business Rule Implementation Mistakes&#8221; <\/p>\n<p>(http:\/\/www.devx.com\/dbzone\/Article\/31985\/)<\/p>\n<h3>Impersonate your user when you test.<\/h3>\n<p>Let&#8217;s suppose that you created a stored procedure but forgot to grant any privileges to execute it. When you, the developer, then unit test your code against your development server, you may connect as a dbo &#8211; this is a common practice. Unfortunately, so long as you connect as dbo, you will not detect your missing permissions. This problem is very easy to fix. For example, you can just create a mock user and add it to your database role.<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">CREATE USER mockuser WITHOUT LOGIN <br \/>\nGO <br \/>\nsp_addrolemember @rolename = &#8216;YourRoleName&#8217;, @membername = &#8216;mockuser&#8217; <br \/>\nGO<\/p>\n<p>In your unit test, impersonate the mock user before executing the stored procedure, as follows:<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">SETUSER &#8216;mockuser&#8217;<\/p>\n<p>If the permission to execute your stored procedure is not granted to either the mock user or to the role, you will get an exception. Do not forget to revert to dbo after running the test:<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">SETUSER<\/p>\n<p><b>Note: <\/b>This is a just very simple example of impersonation. It is good to use when you just run your unit tests on your local machine. It is not a substitute for a more realistic test environment when a real network account is used. A more detailed discussion of database security and impersonation is beyond the scope of this article.<\/p>\n<h3>Populate enough test data before your test.<\/h3>\n<p>We&#8217;ll suppose that you have failed to provide a WHERE clause in your stored procedure:<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">\/*<br \/>\nI deliberately stripped this stored procedure from all the bells and<br \/>\n             whistles that are not relevant to the point I am making.<br \/>\n*\/<br \/>\nCREATE PROCEDURE Writers.DeleteCustomer @CustomerID INT<br \/>\nAS<br \/>\nDELETE FROM Data.Customers &#8211;WHERE clause is missing<br \/>\nRETURN @@ERROR<br \/>\nGO<\/p>\n<p>If you only had one row in your table before running the stored procedure, your error would not be detected by your unit test. Make sure you have enough test data before running the test. After running the test, make sure that all the data except for the one deleted row is still in the table.<\/p>\n<h3>Test how your code handles errors.<\/h3>\n<p>Now suppose that you have changed your DeleteCustomer stored procedure, as follows:<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">CREATE PROCEDURE Writers.DeleteCustomer @CustomerID INT<br \/>\nAS<br \/>\nDECLARE @error INT, @rowcount INT<br \/>\nDELETE FROM Data.Customers WHERE CustomerID = @CustomerID<br \/>\nSELECT @error = @@ERROR&#160; &#8212; forgot to populate @rowcount = @@ROWCOUNT<br \/>\nIF @rowcount = 0 BEGIN<br \/>\n&#160; RAISERROR(&#8216;Could Not Find Customer&#8217;, 16, 1)<br \/>\n&#160; SET @error = -1<br \/>\nEND<br \/>\nRETURN @error<br \/>\nGO<\/p>\n<p>Suppose you want to verify that you get an exception if you provide invalid customer ID, and you have written a unit test in C# using NUnit, as follows:<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">[Test]<br \/>\n[ExpectedException(typeof(SqlException))]<br \/>\npublic void TestDeleteCustomerInvalidID()<br \/>\n{<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SqlCommand cmd = this.connection.CreateCommand();<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; cmd.CommandText = &#8220;EXEC Writers.Delete_Customer -1&#8221;;<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; cmd.ExecuteNonQuery();<br \/>\n}<\/p>\n<p>Apparently the test succeeds, but wait a moment: because the name of the stored procedure is misspelled, you are getting the wrong kind of SQL exception! Your procedure did not even execute, and your unit test should not succeed. The following unit test verifies that the right exception is raised:<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">[Test]<br \/>\npublic void TestDeleteCustomerInvalidID()<br \/>\n{<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; bool gotTheRightException = false;<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; cmd.CommandText = &#8220;EXEC Writers.Delete_Customer -1&#8221;;<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; try<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; {<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; cmd.ExecuteNonQuery();<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; }<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; catch (SqlException e)<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; {<br \/>\n&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;string s = e.Message;<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; gotTheRightException = <br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; s.Contains(&#8220;Could Not Find Customer&#8221;);<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; }<br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; Assert.IsTrue(gotTheRightException);&#160;&#160; <br \/>\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \/\/You still need to verify that no one row is deleted<br \/>\n}<\/p>\n<p>Note that even though you verified that you received the right exception, you still need to verify that no rows are deleted. The reason is simple: <\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">ALTER PROCEDURE Writers.DeleteCustomer @CustomerID INT<br \/>\nAS<br \/>\nDECLARE @error INT, @rowcount INT<br \/>\nDELETE FROM Data.Customers <br \/>\n&#8212; deliberately removed WHERE CustomerID = @CustomerID<br \/>\nSELECT @error = @@ERROR, @rowcount = @@ROWCOUNT<br \/>\nIF @rowcount &lt;&gt; 1 BEGIN<br \/>\n&#160; RAISERROR(&#8216;Could Not Find Customer&#8217;, 16, 1)<br \/>\n&#160; SET @error = -1<br \/>\nEND<br \/>\nRETURN @error<br \/>\nGO<\/p>\n<p>This incorrect procedure first deletes all the rows and then raises the right exception if there were more than one row in the table. For more details on error handling in Transact-SQL refer to articles on Erland Sommarskog&#8217;s Web site. <\/p>\n<p>Testing stored procedures which modify data will be described in more detail in a later article.<\/p>\n<p><b><\/b><\/p>\n<h3>Do Not Forget To Consider Concurrency.<\/h3>\n<p>In some cases, stored procedures, will work as expected as long as you unit test them from a single connection, but fail intermittently in a more realistic multi-connection environment. For example, consider a requirement to implement a stored procedure that should return a unique integer as an output parameter every time it is called.<\/p>\n<p>The following three simple stored procedures all attempt to implement the requirement:<\/p>\n<pre><\/pre>\n<p class=\"MsoNormal\">CREATE TABLE Data.Counter(Counter INT NOT NULL)<br \/>\nINSERT Data.Counter(Counter) VALUES(1)<br \/>\nGO<br \/>\nCREATE PROCEDURE Writers.GetNextCounter1 @NextCounter INT OUT<br \/>\nAS<br \/>\nSET TRANSACTION ISOLATION LEVEL READ COMMITTED<br \/>\nBEGIN TRAN<br \/>\nUPDATE Data.Counter SET Counter = Counter + 1<br \/>\nSELECT @NextCounter = Counter FROM Data.Counter<br \/>\nCOMMIT<br \/>\nGO<br \/>\nCREATE PROCEDURE Writers.GetNextCounter2 @NextCounter INT OUT<br \/>\nAS<br \/>\nSET TRANSACTION ISOLATION LEVEL READ COMMITTED<br \/>\nSELECT @NextCounter = Counter + 1 FROM Data.Counter<br \/>\nUPDATE Data.Counter SET Counter = Counter + 1<br \/>\nGO<br \/>\nCREATE PROCEDURE Writers.GetNextCounter3 @NextCounter INT OUT<br \/>\nAS<br \/>\nSET TRANSACTION ISOLATION LEVEL SERIALIZABLE<br \/>\nBEGIN TRAN<br \/>\nUPDATE Data.Counter SET Counter = Counter + 1<br \/>\nSELECT @NextCounter = Counter FROM Data.Counter<br \/>\nCOMMIT<br \/>\nGO<\/p>\n<p>If you run any one of these three stored procedures from a single connection only, all three would appear to work correctly. However, if you run them thousands of times simultaneously from several connections, the first one will still reliably provide unique numbers, while the second one may provide some duplicates, and the third one is likely to end up in a deadlock. <\/p>\n<p>As you have seen, simple unit testing from one connection is not sufficient in this case.<\/p>\n<p>The next article will explain in more detail how to utilize C# and NUnit to unit test result sets returned from stored procedures.<\/p>\n<div class=\"scheme--lightest-grey spaced-bottom\">\n  <img decoding=\"async\" class=\"media__image padded--tight\" style=\"float: right; vertical-align: middle;\" src=\"https:\/\/assets.red-gate.com\/external\/SimpleTalk\/database-devops-cog-160.png\"><\/p>\n<div class=\"padded\">\n<p class=\"padded-top--tight\"> <strong>DevOps, Continuous Delivery & Database Lifecycle Management<\/strong><br \/> <a href=\"\/collections\/database-lifecycle-management-patterns-practices-library\/?utm_source=simpletalk&amp;utm_medium=publink&amp;utm_campaign=dlm&amp;utm_content=pandparticle&amp;utm_term=bottombanner\">Go to the Simple Talk library<\/a> to find more articles, or visit <a href=\"https:\/\/www.red-gate.com\/solutions?utm_source=simpletalk&amp;utm_medium=publink&amp;utm_campaign=dlm&amp;utm_content=pandparticle&amp;utm_term=bottombanner&amp;__hstc=81186632.9b3ff88e8dc727f9f1f608ffe1681edd.1467735864337.1482415357732.1482419905332.123&amp;__hssc=81186632.36.1482419905332&amp;__hsfp=3698596899\">www.red-gate.com\/solutions<\/a> for more information on the benefits of extending DevOps practices to SQL Server databases.<\/p>\n<\/div>\n<\/div>\n\n","protected":false},"excerpt":{"rendered":"<p>Alex starts of a series of articles on &#8216;Unit Testing&#8217; your database development work. He starts off by describing five simple rules that make all the difference.&hellip;<\/p>\n","protected":false},"author":6776,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143531],"tags":[4178,4168,4150,4252,4810],"coauthors":[6821],"class_list":["post-290","post","type-post","status-publish","format-standard","hentry","category-t-sql-programming-sql-server","tag-bi","tag-database","tag-sql","tag-t-sql-programming","tag-unit-test-tsql-sql-server"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/290","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/6776"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=290"}],"version-history":[{"count":5,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/290\/revisions"}],"predecessor-version":[{"id":68520,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/290\/revisions\/68520"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=290"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=290"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=290"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=290"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}