Dataverse, testy jednostkowe i InvalidPluginExecutionException

Już dawno na blogu nie było żadnego wpisu związanego z programowaniem.  Do dzieła więc!

Wyrzucenie wyjątku jest najpopularniejszym sposobem implementacji przerwania transakcji zaimplementowanych wewnątrz rozszerzeń implementujących interfejs IPlugin platformy Microsoft Dataverse. Wyjątek InvalidPluginOperationException wykorzystywany jest to walidacji danych wejściowych po stronie serwera, obsługi nieprawidłowych przepływów oraz w wielu innych scenariuszach. Każdy niestandardowy kod uruchamiany wewnątrz wspomnianej platformy powinien być oczywiście w miarę możliwości pokryty automatycznie uruchamianymi testami jednostkowymi oraz integracyjnymi. W artykule tym postaram się pokazać, w jaki sposób poprawnie zaimplementować test jednostkowy, który zweryfikuje typ zwracanego przez testowany kod wyjątku.  

Kod prezentowanych poniżej testów wykorzystuje do działania framework MSTest. Tak. Wiem, że jest on stary, zapomniany i mniej funkcjonalny niż NUnit lub XUnit, ale właśnie z niego przyszło mi korzystać w projekcie, nad którym spędziłem kilka ostatnich miesięcy zeszłego roku.    

Wyobraźmy sobie następujące rozszerzenie, które odpowiada za wygenerowanie rozmowy telefonicznej z osobą kontaktową znajdującą się w bazie Dataverse.

public class MakePhoneCallPlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        //...
        var contact = GetContact(contactId); 
        if(contact.Telephone1 != null)
        {
            throw new InvalidPluginExecutionException("Functionality not available"); 
        }
    }
}

Oczywiście nie jest to pełny kod implementujący pełną opisaną powyżej funkcjonalność, a jedynie fragment, który będziemy chcieli przetestować.

Chcielibyśmy stworzyć test jednostkowy, który zweryfikuje, że uruchomienie powyższego kodu dla danych osoby, która nie posiadają numeru telefonicznego, faktycznie spowoduje wyrzucenie wyjątku typu InvalidPluginExecutionException.

Pierwszy pomysł na jego implementację, który na pewno przyjdzie Wam do głowy po przestudiowaniu dokumentacji frameworka MSTest może wyglądać następująco:

[TestMethod]
[ExpectedException(typeof(InvalidPluginExecutionException))]
public void Execute_MissingTelephone_ThrowsException()
{
   ///All the code responsible for setting up plugin execution context

   var plugin = new MakePhoneCallPlugin();
   plugin.Execute(fakeServiceProvider); 
}

Niestety podejście to może być nieprawidłowe. Wyobraźmy sobie sytuację, w której wyjątek InvalidPluginExecutionException spowoduje inna linia kodu niż ta, którą faktycznie chcemy przetestować. W naszym przypadku mógłby być to konstruktor klasy rozszerzenia, zdarzają się jednak oczywiście sytuacje, w których kod testu jest dłuższy niż powyższe 2 linijki. W opisywanej sytuacji test zwróci oczywiście rezultat pozytywny, natomiast z uwagi na to, że do wywołania metody, której działanie chcemy sprawdzić (w naszym przypadku Execute) nigdy nie dojdzie, będzie to informacje nieprawidłowa.

Kolejne podejście, z którym spotkałem się w czasie pracy polega na umieszczaniu wywołania testowanej metody w bloku try-catch i użyciu funkcji Assert w bloku kodu obsługującego przechwycony wyjątek.

[TestMethod]
public void Execute_MissingTelephone_ThrowsException()
{
   ///All the code responsible for setting up plugin execution context

   var plugin = new MakePhoneCallPlugin();
   try
   {
      plugin.Execute(fakeServiceProvider);
   }
   catch (InvalidPluginExecutionException)
   {
      Assert.IsTrue(true, "Exception thrown as expected"); 
   }

   Assert.Fail("Expected exception not thrown"); 
}

W tej sytuacji widzimy, że sprawdzenie wystąpienia wyjątku dotyczy tylko wywołania metody, której zachowanie chcemy badać. Jest już nieco lepiej, natomiast cierpi na tym przejrzystość kodu. Wyobraźmy sobie dziesiątki testów jednostkowych, w których musimy korzystać z podobnej konstrukcji. Statyczne analizatory kodu nie będą z tego powodu zadowolone 😉.

Na szczęście opisaną powyżej instrukcję możemy w łatwy sposób „opakować” do postaci rozszerzenia, którego kod zamieszczam poniżej:

public static class AssertExtension
{
   public static void ThrowsExpectedException<T>(Action action) where T : Exception
   {
      var exceptionThrown = false;
      try
      {
         action.Invoke();
      }
      catch (T)
      {
         exceptionThrown = true;
      }

      if (!exceptionThrown)
      {
         throw new AssertFailedException(
           $"An exception of type { typeof(T) } was expected, but not thrown");
      }
   }
}

Powyższej klasy używamy w następujący sposób:

[TestMethod]
public void Execute_MissingTelephone_ThrowsException()
{
   var plugin = new MakePhoneCallPlugin();
   AssertExtension.ThrowsExpectedException<InvalidPluginExecutionException>(
     () => plugin.Execute(fakeServiceProvider));        
}

Możemy również skorzystać z gotowej metody udostępnianej przez MSTest:

[TestMethod]
public void Execute_MissingTelephone_ThrowsException()
{
   var plugin = new MakePhoneCallPlugin();
   Assert.ThrowsException<InvalidPluginExecutionException>(() => plugin.Execute(fakeServiceProvider));           
}

Wspomniane na początku artykułu biblioteki NUnit oraz XUnit również oferują analogiczny mechanizm.

Total Views: 330 ,