5

The goal is simple - rollback data inserted by a unit test. Here is how it goes. In a unit test, a method is called that creates a new connection and inserts some data. After that a unit test creates a new connection and tries to find what has been inserted and assert that. I was hoping to wrap these two things with TransactionScope, not call Complete and see inserted data rolled back. That's not happening. Am I doing something wrong or I am just missing the point?

using (new TransactionScope())
{
    // call a method that inserts data
    var target = new ....
    target.DoStuffAndEndupWithDataInDb();

    // Now assert what has been added.
    using (var conn = new SqlConnection(connectionString))
    using (var cmd = conn.CreateCommand())
    {
        // Just read the data from DB
        cmd.CommandText = "SELECT...";
        conn.Open();
        int count = 0;
        using (var rdr = cmd.ExecuteReader())
        {
            // Read records here
            ...
            count++;
        }

        // Expecting, say, 3 records here
        Assert.AreEqual(3, count);
    }
}

EDIT: I don't think I had DTC running and configured on my machine. So I started the service and tried to configure DTC but I am getting this error. enter image description here

Schultz9999
  • 8,717
  • 8
  • 48
  • 87
  • 1
    your unit tests really shouldn't touch any external dependencies like the database. – Daniel A. White Nov 21 '11 at 19:30
  • Sure. Consider that an integration test. BTW, in reality I may not use SQL connection but instead use in-memory DB adapter. So while your comments are true in general, they are not helpful to answer the question. Furthermore, this is just one use case that can be applied to NOT unit test but normal algorithmic workflow in a program. – Schultz9999 Nov 21 '11 at 19:31
  • Did you trace the instructions that are being send to the Databases (SQL Profiler)? – Emond Nov 21 '11 at 19:33
  • @Erno: no. That's a great suggestion. I suppose I won't see any rollbacks there. And that's why I wonder if I am doing something wrong or this is not designed to work that way. – Schultz9999 Nov 21 '11 at 19:35
  • No, you didn't miss the point. You definitely should not be seeing your data committed to the database if you are not calling Complete(). What is actually going on inside of the "DoStuff..." method? Also, do you actually need to have two separate connection objects for your test, or could you use a single SqlConnection and SqlTransaction across the whole test? – Michael Edenfield Nov 21 '11 at 19:41
  • @Schultz9999 You definitely should see a commit or rollback issued in Profiler, if you have the correct options installed. I believe you need to look in the TransactionsCategory, and enable DTCTransactions (I usually enable SQLTransactions as well). – Michael Edenfield Nov 21 '11 at 19:43
  • @MichaelEdenfield: using a single connection and transaction is usually the way I do that. That does require though an internal constructor to pass these two. This sometimes causes passing by the ctor that is normally expected to be called. But this works great. I was just hoping that TransactionScope can help me to simplify this and eliminate a need to write more internal code for testing purposes only. – Schultz9999 Nov 21 '11 at 19:44
  • Weird, today I did exactly what you did and it rolled back. – Piotr Perak Nov 21 '11 at 20:01
  • You can write a ConnectionManager with a singleton pattern in the context of the current thread that can hold a connection if you don't want to pass it around. – Magnus Nov 21 '11 at 20:27

4 Answers4

1

I don't think you're missing the point but just attacking the problem incorrectly.

In NUnit terms, the concepts are [SetUp] and [TearDown] methods. You've already defined the setup method in your description and your tear down method should just undo what the setup method did (assuming what you're unit testing has no residual side effects).

Austin Salonen
  • 49,173
  • 15
  • 109
  • 139
  • Hmm... That could be done. This somewhat eliminates all needs for transactions because then I had to do explicit cleanup, which I was hoping to delegate to rollback. – Schultz9999 Nov 21 '11 at 19:41
1

Do you have Distributed Transaction Coordinator properly configured? This is a big gotcha when trying to use TransactionScope like this... if it isn't configured, sometimes you'll get an error, but other times the transaction will just commit and not rollback.

I'd recommend looking at this article, which shows you all the various steps that need to be done in order to rollback your unit tests using MSDTC.

Michael Fredrickson
  • 36,839
  • 5
  • 92
  • 109
  • I tried what you suggested but no luck to configure DTC on my Windows 7 box. The service itself was down so I started the service. However I am not able so far to change properties of in the Component Services Snap-In (see the image above). – Schultz9999 Nov 21 '11 at 22:36
1

are you using MSTest ? then you can use MsTestExtensions you unit test needs to derive from MSTestExtensionsTestFixture and your test needs to have TestTransaction Attribute, it uses AOP to automatically start a transaction and roll it back.

np-hard
  • 5,725
  • 6
  • 52
  • 76
0

Your code should work as you expect. How are you adding data in DoStuffAndEndupWithDataInDb()? I'm wondering whether the data initialization is not participating in the transaction.

For reference, the following console application correctly outputs 3 rows, and does not commit the rows to the database (checked using SSMS).

public class Program
{
    private static void Main(string[] args)
    {
        using (var trx = new TransactionScope())
        {
            InitializeData();

            using (var connection = new SqlConnection("server=localhost;database=Test;integrated security=true"))
            using (var command = connection.CreateCommand())
            {
                command.CommandText = "select count(*) from MyTable";
                connection.Open();
                Console.WriteLine("{0} rows", command.ExecuteScalar());
            }
        }
        Console.ReadLine();
    }

    private static void InitializeData()
    {
        using (var connection = new SqlConnection("server=localhost;database=Test;integrated security=true"))
        using (var command = connection.CreateCommand())
        {
            command.CommandText = "insert into MyTable values (1),(2),(3)";
            connection.Open();
            command.ExecuteNonQuery();
        }
    }
}
Jeff Ogata
  • 56,645
  • 19
  • 114
  • 127
  • Multiple connection inside a transaction scope only works if the distributed transaction manager is enabled on db and client. Perhaps settings pooling to true and max connection in pool to 1 might work, but not efficient. – Magnus Nov 21 '11 at 20:33
  • @Magnus, do you mean the Distributed Transaction Coordinator service? – Jeff Ogata Nov 21 '11 at 20:44
  • 1
    @Magnus I wouldn't think DTC would be needed here since the connections are both to the same database. I did have DTC running, but stopped it and the code above still works. – Jeff Ogata Nov 21 '11 at 20:51
  • According to MSDN: "If there are multiple connections, the TransactionScope class considers the transaction to be a distributed transaction". But maybe that only applies if the connection overlaps, not sure. – Magnus Nov 21 '11 at 21:30
  • @Magnus: DTC wasn't started on my machine but was on SQL box. Unfortunately starting it on my machine made no difference... I am a bit frustrated because adrift says it works in his case. – Schultz9999 Nov 21 '11 at 22:05
  • @adrift: Nothing special when inserting data. Using CommandText `INSERT INTO dbo.MyTable (FieldName) VALUES (@value)` and then `cmd.Parameters.AddWithValue`. – Schultz9999 Nov 21 '11 at 22:06
  • @Schultz9999 if you try code like this in a console application, does it work as expected? – Jeff Ogata Nov 21 '11 at 22:37
  • @adrift does your example still work if you set Pooling to false in the connection string? – Magnus Nov 22 '11 at 08:35
  • @Magnus, in that case, it does treat the transaction as distributed. If DTC is running, the code works as expected. If DTC is stopped, I get a SqlException: `MSDTC on server is unavailable.` – Jeff Ogata Nov 22 '11 at 14:39
  • @adrift So it is just pure luck that you get the same connection from the pool when you create the 2'nd connection. If you have multiple users accessing the application that might not be the case. – Magnus Nov 22 '11 at 16:01
  • @Magnus, not sure what you're getting at; he's running this in an integration test. – Jeff Ogata Nov 22 '11 at 16:35