How do I recover when tSQLt faketable does not reset table mapping?

瘦欲@ 提交于 2020-03-17 05:55:08

问题


faketable function did not reassign to normal. All my tables I used faketable on now contain the content of the values I used in the insert of the unit test. It was many tables and it has left my database useless. Please help address this problem or at least its cause. This makes me very nervous about using this in our CI deployment process and maybe more importantly in our local development efforts.


回答1:


It is possible one of your tests or your code, left the transaction in a state where it could not be rolled back. This would typically result in seeing one or more tests with an "Error" (instead of "Success" or "Failure") in the results.

In these cases, the FakeTable operation is not rolled back, and the tables are left in their faked state.

Under the covers, FakeTable renames the table and creates a new copy of it. When the rename happens, the operation is logged in the tSQLt.Private_RenamedObjectLog.

For example, you can use the following code to reproduce an error that tSQLt cannot gracefully rollback from:

EXEC tSQLt.NewTestClass 'SOF_Example'
GO

CREATE TABLE SOF_Example.MyTable (i INT);
GO

INSERT INTO SOF_Example.MyTable (i) VALUES (5);
GO

CREATE PROCEDURE SOF_Example.[test fake a table]
AS
BEGIN
    EXEC tSQLt.FakeTable 'SOF_Example.MyTable';

    INSERT INTO SOF_Example.MyTable (i) VALUES (12);

    COMMIT;
END;
GO

EXEC tSQLt.Run 'SOF_Example';

You can use this code to look into the renamed table log:

SELECT OriginalName, SCHEMA_NAME(schema_id) + '.' + name AS [Name of Renamed Table], create_date
FROM tSQLt.Private_RenamedObjectLog
JOIN sys.objects ON ObjectId = object_id;

If you've re-executed the tests many times, you may have many entries in the log for each faked table. You can use the create_date to help determine which one contains the original data.

Now, with all that said: It is best to not write and execute test cases in a database where you must preserve the data. The best approach is to use a database that contains no user data (only the essential configuration data at most). You should be developing and unit testing out of a blank database. Populated databases should be used for other forms of testing, such as integration, usability, performance, etc.




回答2:


This only handles putting tables back into place (because that's the problem the OP and I both had) but monkeying with the line that drops the table will probably get it working with other object types.

DECLARE @cmd nvarchar(MAX) = '';

WITH x AS (
    SELECT TOP 10000 
           PL.Id                                            AS Id
          ,PARSENAME(PL.OriginalName,1)                     AS OriginalName
          ,ISNULL(SO.name,'')                               AS name
          ,QUOTENAME(SCHEMA_NAME(ISNULL(SO.schema_id,1)))   AS SchemaName
          ,ISNULL(SEP.major_id,-1)                          AS major_id
      FROM tSQLt.Private_RenamedObjectLog PL
      LEFT JOIN sys.objects SO
        ON ObjectId = object_id
      LEFT JOIN sys.extended_properties SEP
        ON SEP.major_id = SO.object_id
       AND SEP.name = 'tSQLt.FakeTable_OrgTableName'
     ORDER BY SO.create_date DESC
)
SELECT @cmd = @cmd 
       + CASE WHEN x.name = '' OR OriginalName = x.name 
              THEN N'DELETE tSQLt.Private_RenamedObjectLog WHERE Id = ' + CAST(x.Id AS nvarchar) + N';'
              ELSE N'DROP ' 
                 + N'TABLE'   --Replace this with a CASE statement to deal with other object types
                 + N' ' + SchemaName + '.' + QUOTENAME(x.OriginalName) + '; ' 
                 + NCHAR(13) + NCHAR(10) + N'EXEC sp_rename ''' + SchemaName + N'.' 
                                         + QUOTENAME(x.name) + N''',''' + OriginalName + N''';'
                 + NCHAR(13) + NCHAR(10) + N'IF OBJECT_ID('''+SchemaName + N'.' + QUOTENAME(x.name)+N''') IS NULL'
                 + NCHAR(13) + NCHAR(10) + N'BEGIN'
                 + CASE WHEN x.major_id != -1 
                        THEN NCHAR(13) + NCHAR(10) + N'    EXEC sp_dropextendedproperty ''tSQLt.FakeTable_OrgTableName'',''SCHEMA'',''' 
                           + PARSENAME(SchemaName,1) + N''',''TABLE'',''' + OriginalName + N''';'
                        ELSE ''
                   END
                 + NCHAR(13) + NCHAR(10) + N'    DELETE tSQLt.Private_RenamedObjectLog WHERE Id = ' + CAST(x.Id AS nvarchar) + N';'
                 + NCHAR(13) + NCHAR(10) + N'END'
         END
       + NCHAR(13) + NCHAR(10)
       + NCHAR(13) + NCHAR(10)
  FROM x;

--/*   <-Remove leading dashes to execute
PRINT @cmd;
--*/EXEC (@cmd);



回答3:


I had the same problem with tSQLt, and was able to restore everything using the contents of the table tSQLt.Private_RenamedObjectLog

This table is maintained by the tSQLt framework, and proved to contain the names of the original tables which had been faked, and the SQL ObjectIDs of the temporary (i.e. fake) tables. Using the following query produced the list of the faked tables, and the names that they had been temporarily renamed to (random names produced by tSQLt such as tSQLt_tempobject_3815e077fea84c7c):

SELECT        
  ObjectId, OriginalName, 
  OBJECT_SCHEMA_NAME(ObjectId) AS SchemaName, 
  OBJECT_NAME(ObjectId) AS TemporaryName
FROM            
  tSQLt.Private_RenamedObjectLog

Refreshing the object explorer in SSMS showed that tables with these random names did indeed exist, and they did indeed contain my original data (whew!!).

I then did the following:

  1. Tried a ROLLBACK TRANSACTION just in case that sorted it out. It didn't.
  2. BACKED UP THE DATABASE (even though it was messed up)
  3. Dropped the fake tables (i.e. the ones with the original names, not the temporary names)
  4. Renamed the tables (with temporary names) back to their original names, using this for each table:

    EXEC sp_rename 'schema.tempname', 'originalname'

  5. Cleared the table tSQLt.Private_RenamedObjectLog after I knew my tables were back, using

    DELETE FROM tSQLt.Private_RenamedObjectLog

It would be easy to make a procedure to produce a restore script automatically! Maybe there is one already in tSQLt - anyone know about that?




回答4:


I'm aware I've answered this question before! Hopefully this answer is more useful. Also that the question is old. No one else has addressed faking functions, or procedures with SpyProcedure.

Yes, this problem is easily caused by running the code of a tSQLt test outside of a transaction. tSQLt runs every test in a transaction so all faking etc. gets rolled back at the end of the test. tSQLt renames tables etc. in order to fake them, and this usually gets rolled back. Running SpyProcedure also creates a log table, which can clash when you try and run SpyProcedure again. So if you don't run the test in a transaction, these changes don't get rolled back. Fortunately, tSQLt contains all the information needed to restore everything.

First try a ROLLBACK TRANSACTION in case that sorts it out. If not, check if your original tables etc are recorded in tSQLt's table for faked/renamed stuff:

SELECT ObjectId, OriginalName,
  OBJECT_SCHEMA_NAME(ObjectId) AS SchemaName, OBJECT_NAME(ObjectId) AS TemporaryName
FROM tSQLt.Private_RenamedObjectLog

If any records are returned, there's a good chance things can be restored. But please back up your database first!

Now we generate SQL code to delete the faked objects (after testing the original still exists) and rename the originals back to their real names. As far as I know, tSQLt can fake tables, views and functions, as well as procedures using SpyProcedure, so this should work in almost all situations. Log tables created by SpyProcedure are also deleted:

DECLARE @SQL_work NVARCHAR(MAX) = '';
DECLARE @template NVARCHAR(MAX) = 
'IF OBJECT_ID(''%s.%s'') IS NOT NULL BEGIN -- check original still exists
  DROP %s %s.%s   -- deleted faked object
  EXEC sp_rename ''%s.%s'', ''%s''
END
';
SELECT
  @SQL_work = @SQL_work + 
    FORMATMESSAGE (CAST (@template AS NVARCHAR (MAX)),
      QUOTENAME(OBJECT_SCHEMA_NAME(r.ObjectId)), OBJECT_NAME(ObjectId),
      CASE WHEN type IN ('U', 'V') THEN 'TABLE'
           WHEN type IN ('FN','FS','TF','IF','FT') THEN 'FUNCTION'
           WHEN type = 'P' THEN 'PROCEDURE'
      END, 
      QUOTENAME(OBJECT_SCHEMA_NAME(r.ObjectId)), OriginalName,
      QUOTENAME(OBJECT_SCHEMA_NAME(r.ObjectId)), OBJECT_NAME(ObjectId),
      PARSENAME (OriginalName, 1)
    ) +
    CASE WHEN type = 'P' 
         THEN FORMATMESSAGE ('DROP TABLE %s.%s_SpyProcedureLog
',              QUOTENAME(OBJECT_SCHEMA_NAME(r.ObjectId)),
                PARSENAME (OriginalName, 1)
              )
         ELSE ''
    END +CHAR(13)+CHAR(10)
FROM
  tSQLt.Private_RenamedObjectLog AS r
JOIN 
  sys.objects AS o ON r.ObjectId = o.object_id
PRINT @SQL_work
PRINT 'DELETE FROM tSQLt.Private_RenamedObjectLog'

The above generates and prints code to set things back. I recommend executing the generated script cautiously i.e. execute the parts one by one, and only execute the last DELETE statement if everything else worked.

I'd be very interested to know if this worked for you, or if any problems were encountered so that the code can be improved.



来源:https://stackoverflow.com/questions/12161106/how-do-i-recover-when-tsqlt-faketable-does-not-reset-table-mapping

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!