2015-06-05 11 views
10

Z powodu kilku ograniczeń nie mogę korzystać z encji podmiotu, dlatego należy ręcznie korzystać z połączeń, poleceń i transakcji SQL.Testy jednostkowe z transakcjami ręcznymi i transakcjami wielowarstwowymi

Podczas pisania testów jednostkowych metod wywoływania tych operacji warstwy danych natknąłem się na kilka problemów.

Do testów jednostkowych I POTRZEBUJEMY, aby wykonać je w Transakcji, ponieważ większość operacji zmienia dane ze względu na ich charakter, a zatem wykonywanie ich poza Transakcją jest problematyczne, ponieważ spowoduje to zmianę wszystkich podstawowych danych. W związku z tym muszę umieścić transakcję wokół nich (bez zatwierdzenia commit na końcu).

Teraz mam 2 różne warianty działania tych metod BL. Niektóre z nich zawierają same transakcje, podczas gdy inne nie zawierają żadnych transakcji. Oba warianty powodują problemy.

  • warstwami transakcja: Tutaj mam błędy DTC anulował transakcję rozproszoną z powodu przekroczenia limitu czasu (choć limit czasu jest ustawiony na 15 minut i jest on uruchomiony tylko 2 minuty).

  • Tylko 1 Transakcja: Tutaj pojawia się błąd dotyczący stanu transakcji, gdy przychodzę do linii "new SQLCommand" w wywołanej metodzie.

Moje pytanie brzmi: co mogę zrobić, aby to poprawić i uzyskać testowanie jednostkowe z ręcznymi normalnymi i wielowarstwowymi transakcjami?

Jednostka Metoda badania przykład:

using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString)) 
{ 
    connection.Open(); 
    using (SqlTransaction transaction = connection.BeginTransaction()) 
    { 
     MyBLMethod(); 
    } 
} 

Przykład transakcję za pomocą metody (uproszczonej)

using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString)) 
{ 
    connection.Open(); 
    using (SqlTransaction transaction = connection.BeginTransaction()) 
    { 
     SqlCommand command = new SqlCommand(); 
     command.Connection = connection; 
     command.Transaction = transaction; 
     command.CommandTimeout = 900; // Wait 15 minutes before a timeout 
     command.CommandText = "INSERT ......"; 
     command.ExecuteNonQuery(); 

     // Following commands 
     .... 

     Transaction.Commit(); 
    } 
} 

przykład dla nie transakcji przy użyciu metody

using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString)) 
{ 
    connection.Open(); 

    SqlCommand command = new SqlCommand(); 
    command.Connection = connection; 
    command.CommandTimeout = 900; // Wait 15 minutes before a timeout 
    command.CommandText = "INSERT ......"; 
    command.ExecuteNonQuery(); 
} 
+1

Ponieważ musisz użyć 'SqlConnection', nie możesz testować jednostki; robisz testy integracyjne. Więc zamiast robić wszystko, ale zatwierdzaj, użyj testu DB (który możesz skonstruować od nowa na początku testu) i przetestuj interakcję DB. –

+0

Co powiedział. Powinieneś używać migawek bazy danych do tworzenia czystych baz danych za każdym razem. – Ewan

+0

Oznacza to, że musiałbym odtworzyć całą (prawdopodobnie obejmującą wiele GB danych) bazy danych dla każdego uruchomienia testu jednostkowego? b (który może podwoić lub potroić czas pracy do kilku minut) – Thomas

Odpowiedz

4

na twarzy z tego, masz kilka opcji, w zależności od tego, co chcesz przetestować i zdolności do spe nd money/zmień bazę kodu.

W tej chwili skutecznie piszesz testy integracji. Jeśli baza danych nie jest dostępna, testy zakończą się niepowodzeniem. Oznacza to, że testy mogą być powolne, ale na plusie, jeśli przejdą, jesteś pewny, że kod może trafiać poprawnie w bazę danych.

Jeśli nie masz nic przeciwko trafianiu w bazę danych, to minimalny wpływ na zmianę kodu/pieniędzy na wydawanie pieniędzy będzie dla Ciebie, aby transakcje mogły zostać ukończone i zweryfikowane w bazie danych. Możesz to zrobić, wykonując migawki bazy danych i resetując bazę danych przy każdym uruchomionym teście, lub korzystając z dedykowanej bazy danych testów i zapisując testy w taki sposób, aby mogły bezpiecznie trafiać w bazę danych w kółko, a następnie weryfikować. Na przykład można wstawić rekord z zwiększonym identyfikatorem, zaktualizować rekord, a następnie sprawdzić, czy można go odczytać. Być może będziesz miał więcej do zrobienia, jeśli wystąpią błędy, ale jeśli nie modyfikujesz kodu dostępu do danych lub struktury bazy danych, to często nie powinno to stanowić problemu.

Jeśli jesteś w stanie wydać trochę pieniędzy i chcesz zamienić swoje testy na testy jednostkowe, aby nie trafiały do ​​bazy danych, powinieneś rozważyć zaglądanie w TypeMock.To bardzo potężny szyderczy framework, który może zrobić trochę przerażających rzeczy. Sądzę, że używa interfejsu API do przechwytywania wywołań, zamiast używać podejścia stosowanego przez struktury takie jak Moq. Istnieje przykład użycia Typemock do sfałszowania połączenia SQLConnection here.

Jeśli nie masz pieniędzy do wydania/możesz zmienić kod i nie musisz już dłużej polegać na bazie danych, to musisz poszukać sposobu na udostępnienie połączenia z bazą danych między kodem testowym i twoje metody dostępu do danych. Dwa podejścia, które przychodzą na myśl, to albo wprowadzić informacje o połączeniu do klasy, albo udostępnić je poprzez wstrzyknięcie fabryki, która daje dostęp do informacji o połączeniu (w takim przypadku można wstrzyknąć próbkę z fabryki podczas testów, która zwraca połączenie chcesz).

Jeśli wybierzesz powyższe podejście, zamiast bezpośrednio wstrzykiwać SqlConnection, rozważ wstrzyknięcie klasy opakowania, która jest również odpowiedzialna za transakcję. Coś takiego jak:

public class MySqlWrapper : IDisposable { 
    public SqlConnection Connection { get; set; } 
    public SqlTransaction Transaction { get; set; } 

    int _transactionCount = 0; 

    public void BeginTransaction() { 
     _transactionCount++; 
     if (_transactionCount == 1) { 
      Transaction = Connection.BeginTransaction(); 
     } 
    } 

    public void CommitTransaction() { 
     _transactionCount--; 
     if (_transactionCount == 0) { 
      Transaction.Commit(); 
      Transaction = null; 
     } 
     if (_transactionCount < 0) { 
      throw new InvalidOperationException("Commit without Begin"); 
     } 
    } 

    public void Rollback() { 
     _transactionCount = 0; 
     Transaction.Rollback(); 
     Transaction = null; 
    } 


    public void Dispose() { 
     if (null != Transaction) { 
      Transaction.Dispose(); 
      Transaction = null; 
     } 
     Connection.Dispose(); 
    } 
} 

Spowoduje to zatrzymanie transakcji zagnieżdżonych + zatwierdzonych.

Jeśli chcesz bardziej zrestrukturyzować swój kod, możesz chcieć owijać kod dostępu do danych w sposób bardziej wydajny. Tak, na przykład, możesz wcisnąć swoją podstawową funkcję dostępu do bazy danych do innej klasy. W zależności od tego, co robisz, musisz rozwijać na nim, jednak może skończyć się z czymś takim:

public interface IMyQuery { 
    string GetCommand(); 
} 

public class MyInsert : IMyQuery{ 
    public string GetCommand() { 
     return "INSERT ..."; 
    } 
} 

class DBNonQueryRunner { 
    public void RunQuery(IMyQuery query) { 
     using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString)) { 
      connection.Open(); 
      using (SqlTransaction transaction = connection.BeginTransaction()) { 
       SqlCommand command = new SqlCommand(); 
       command.Connection = connection; 
       command.Transaction = transaction; 
       command.CommandTimeout = 900; // Wait 15 minutes before a timeout 
       command.CommandText = query.GetCommand(); 

       command.ExecuteNonQuery(); 

       transaction.Commit(); 
      } 
     } 
    } 
} 

Pozwala to jednostka testowa więcej z logiki, jak kod generacji polecenia bez konieczności martwienia się o trafienie w bazę danych i możesz przetestować swój podstawowy kod dostępu do danych (Runner) w bazie danych raz, a nie za każde polecenie, które chcesz uruchomić w bazie danych. Nadal będę pisać testy integracyjne dla całego kodu dostępu do danych, ale będę je raczej uruchamiał podczas pracy nad tą sekcją kodu (aby zapewnić, że nazwy kolumn itp. Zostały poprawnie określone).

+0

+1 na temat tego, co @forsvarir mówi. Zawsze możesz testować rzeczy, dodając kolejną warstwę pośrednią :) Ale hej, jest też TypeMock, jeśli masz ochotę. –

Powiązane problemy