5

This is my first day with tsqlt so you can expect some vague statements.

I am trying to test a storedProcedure which has a Try Catch Block but the actual statements in test are insert and update command.

now I want to test if in case there was an ErrorRaised does my catch block performs the expected tasks.

Can you please guide me on how do I Raise an Error from a stored procedure in test where we do not have anything to mock/fake inside.

Hope my question is understandable, happy to clarify if needed.

Muds
  • 4,006
  • 5
  • 31
  • 53

2 Answers2

3

So if I understand your question correctly, you are trying to test that your catch block works?

The way to do this will depend on what happens within your catch block. Imagine this scenario:

create table mySimpleTable
(
  Id int not null primary key
, StringVar varchar(8) null 
, IntVar tinyint null
)
go

We have a stored procedure that inserts data into this table thus.

This is based on a template that I use in many of my procedures. It starts by validating the input(s), then does the work it needs to do. Naming each step can be particularly useful to understand where the error occurred in more complex, multi-step procedures. The catch block uses my Log4TSql logging framework that you can read more about on my blog and download from SourceForge.

The pattern I follow is to capture information about the exception and what the procedure was doing at the time of the error within the catch block but ensure that an error is still thrown at the end of the procedure. You could also choose to call raiserror (also throw on SQL2012) within the catch block. Either way, I believe that if a procedure hits an exception it should always be notified up the chain (i.e. never hidden).

create procedure mySimpleTableInsert
(
  @Id int
, @StringVar varchar(16) = null
, @IntVar int = null
)
as
begin
    --! Standard/ExceptionHandler variables
    declare @_FunctionName nvarchar(255) = quotename(object_schema_name(@@procid))
             + '.' + quotename(object_name(@@procid));
    declare @_Error int = 0;
    declare @_ReturnValue int;
    declare @_RowCount int = 0;
    declare @_Step varchar(128);
    declare @_Message nvarchar(1000);
    declare @_ErrorContext nvarchar(512);

    begin try
        set @_Step = 'Validate Inputs'
        if @Id is null raiserror('@Id is invalid: %i', 16, 1, @Id);

        set @_Step = 'Add Row'
        insert dbo.mySimpleTable (Id, StringVar, IntVar)
        values (@Id, @StringVar, @IntVar)
    end try
    begin catch
        set @_ErrorContext = 'Failed to add row to mySimpleTable at step: '
                 + coalesce('[' + @_Step + ']', 'NULL')

        exec log4.ExceptionHandler
                  @ErrorContext   = @_ErrorContext
                , @ErrorProcedure = @_FunctionName
                , @ErrorNumber    = @_Error out
                , @ReturnMessage  = @_Message out
        ;
    end catch

    --! Finally, throw any exception that will be detected by the caller
    if @_Error > 0 raiserror(@_Message, 16, 99);

    set nocount off;

    --! Return the value of @@ERROR (which will be zero on success)
    return (@_Error);
end
go

Let's start by creating a new schema (class) to hold our tests.

exec tSQLt.NewTestClass 'mySimpleTableInsertTests' ;
go

Our first test is the simplest and just checks that even if an exception is caught by our catch block, an error is still returned by the procedure. In this test we simply use exec tSQLt.ExpectException to check that an error is raised when @Id is supplied as NULL (which fails our input validation checks)

create procedure [mySimpleTableInsertTests].[test throws error from catch block]
as
begin
    exec tSQLt.ExpectException @ExpectedErrorNumber = 50000;

    --! Act
    exec dbo.mySimpleTableInsert @Id = null
end;
go

Our second test is a little more complex and makes use of tsqlt.SpyProcedure to "mock" the ExceptionHandler that would otherwise record the exception. Under the hood, when we mock a procedure in this way, tSQLt creates a table named after the procedure being spied and replaces the spied procedure with one that just writes the input parameter values to that table. This is all rolled back at the end of the test. This allows us to can check that ExceptionHandler was called and what values were passed into it. In this test we check that ExceptionHander was called by mySimpleTableInsert as a result of an input validation error.

create procedure [mySimpleTableInsertTests].[test calls ExceptionHandler on error]
as
begin
    --! Set the Error returned by ExceptionHandler to zero so the sproc under test doesn't throw the error
    exec tsqlt.SpyProcedure 'log4.ExceptionHandler', 'set @ErrorNumber = 0;';

    select
          cast('Failed to add row to mySimpleTable at step: [Validate inputs]' as varchar(max)) as [ErrorContext]
        , '[dbo].[mySimpleTableInsert]' as [ErrorProcedure]
    into
        #expected

    --! Act
    exec dbo.mySimpleTableInsert @Id = null

    --! Assert
    select
          ErrorContext
        , ErrorProcedure
    into
        #actual
    from
        log4.ExceptionHandler_SpyProcedureLog;

    --! Assert
    exec tSQLt.AssertEqualsTable '#expected', '#actual';
end;
go

Finally the following (somewhat contrived) examples use the same pattern to check that an error is caught and thrown if the value of @IntVar is too big for the table:

create procedure [mySimpleTableInsertTests].[test calls ExceptionHandler on invalid IntVar input]
as
begin
    --! Set the Error returned by ExceptionHandler to zero so the sproc under test doesn't throw the error
    exec tsqlt.SpyProcedure 'log4.ExceptionHandler', 'set @ErrorNumber = 0;';

    select
          cast('Failed to add row to mySimpleTable at step: [Add Row]' as varchar(max)) as [ErrorContext]
        , '[dbo].[mySimpleTableInsert]' as [ErrorProcedure]
    into
        #expected

    --! Act
    exec dbo.mySimpleTableInsert @Id = 1, @IntVar = 500

    --! Assert
    select
          ErrorContext
        , ErrorProcedure
    into
        #actual
    from
        log4.ExceptionHandler_SpyProcedureLog;

    --! Assert
    exec tSQLt.AssertEqualsTable '#expected', '#actual';
end;
go
create procedure [mySimpleTableInsertTests].[test throws error on invalid IntVar input]
as
begin
    exec tSQLt.ExpectException @ExpectedErrorNumber = 50000;

    --! Act
    exec dbo.mySimpleTableInsert @Id = 1, @IntVar = 500
end;
go

If this doesn't answer your question, perhaps you could post an example of what you are trying to achieve.

datacentricity
  • 1,099
  • 9
  • 15
  • Thanks DataCentricity, for your detailed explanation, this sounds something that I should follow, but one doubt that I still have is, where we do if @Id is null raise an error, can we simulate that from our test procedure ? I mean what if my stored proc dosent raises any error nor is there any thing to mock to raise an error ? how do we test such scenarios ? Hope I am making sense.. Thanks Again. – Muds Apr 10 '15 at 09:50
  • @Muds Even if `mySimpleTableInsert` doesn't have the "Validate Inputs" step, `[test throws error from catch block]` will still test that an error is thrown because Id on mySimpleTable is constrained to be not null. Can you post an example of the type of thing you are trying to test as if I can understand how your code works, especially in the catch block, I might be able to provide you with a more specific answer. – datacentricity Apr 10 '15 at 10:02
  • Also, my second 2 test examples (added later) show you how to test an error on the insert itself. In the statement `exec dbo.mySimpleTableInsert @Id = 1, @IntVar = 500` the value of @IntVar is deliberately too big for the target column which is a tinyint. This will force an error which you can test with `tSQLt.ExpectException` – datacentricity Apr 10 '15 at 10:13
  • hmm, so you reckon, to get the error raised, we will have to break some or other condition we cant just mock something in our test to raise an error. – Muds Apr 10 '15 at 10:18
  • Of course. Sometimes you have to contrive a situation in a test that might not happen in the real world. In the above example (assuming `mySimpleTable` hasn't been faked) even if your sproc doesn't validate inputs the way mine does, if you pass @Id in as NULL you will get an exception. Your test proves that in the event of the sproc encountering an error, that an exception is returned to the caller. Is my understanding of what your are trying to assert correct? – datacentricity Apr 10 '15 at 10:24
  • yes your understanding is spot on ..... guess now I kno what I was looking for is not possible in current testing frameworks, but surely all your inputs would help me .. – Muds Apr 10 '15 at 10:38
  • Guess this is the way forward, thanks for your input ! – Muds Apr 23 '15 at 14:30
1

You use RAISERROR in SQL Server to achieve this:

RAISERROR ('Error raised in TRY block.', -- Message text.
               16, -- Severity.
               1 -- State.
               );

You can see more information on the MSDN website: RAISERROR

Christian Barron
  • 2,695
  • 1
  • 14
  • 22
  • I want this error to be raised when we are testing my StoredProc, so that catch statement in sp could be tested. – Muds Apr 09 '15 at 10:35
  • The error has to occur in the Stored Procedure for it to be caught in there. You would have to add RAISERROR in the try statement within the sp. If you're trying to create an error outside the sp in whatever calls it then you will need a separate try catch block for that. Sp will only handle it's own errors not the callers. – Christian Barron Apr 09 '15 at 10:38
  • well in that case how do we test such scenarios ? – Muds Apr 09 '15 at 10:41
  • like I say just add the RAISERROR statement above into your SP Try block and then this will invoke the catch block. Once you've finished testing it you can remove this statement from the SP – Christian Barron Apr 09 '15 at 10:43
  • alter proc and then realter ? I am not sure if that's the best way to go fwd, if that only way fwd I will be disappointed. – Muds Apr 09 '15 at 10:47