Question

J'apprends le motif Modèle / View / ViewModel et ses variantes (DataModel / View / ViewModel ou Model / View / Presenter).

Ce que je me demande, c'est: si j'utilise ce modèle avec un service WCF, le service est-il un modèle (modèle de données) ou ai-je besoin d'un modèle séparé pour encapsuler la couche de service WCF?

Lorsque j'utilise WCF en tant que DataModel, mon ViewModel n'est pas testable sans se moquer de tout le service WCF, car les appels à WCF doivent gérer la connexion. Les appels dans ce ViewModel ressemblent à ceci:

List<Sam.Alyza.WcfInterface.Website> rc = null;
Service<Sam.Alyza.WcfInterface.IServiceWebsites>.Use(alyzaSvc =>
{
  rc = new List<Sam.Alyza.WcfInterface.Website>(alyzaSvc.GetSites());
});

Pour que mon ViewModel soit testable, j'ai essayé d'ajouter un DataModel séparé pour abstraire la connexion WCF. Après cela, le ViewModel était testable, les appels ressemblaient à ceci:

List<Sam.Alyza.WcfInterface.Website> rc = new List<Sam.Alyza.WcfInterface.Website>(_datamodel.GetSites());

Problème: la plupart du code à tester maintenant avait été transféré dans le DataModel, qui, là encore, nécessitait le test de WCF. Ce qui restait dans le ViewModel était une coque mince, qui pouvait être testée. Mais comme le code principal a été transféré dans le DataModel, le test du ViewModel s’est avéré totalement inutile.

Il me semble donc que l'ajout d'une couche de modèle de données distincte à une application View / ViewModel à l'aide de WCF demande beaucoup de travail, mais la testabilité ne s'améliore pas.

Était-ce utile?

La solution

Woot, après avoir passé deux jours à régler ce problème, j’ai trouvé une solution avec laquelle je peux vivre:

Comme indiqué dans l'exemple de code ci-dessus, j'utilise cette classe d'assistance pour gérer ma connexion WCF (en raison du traitement approprié de Close vs Abort):

public delegate void UseServiceDelegate<T>(T proxy);

public static class Service<T>
{
  public static ChannelFactory<T> _channelFactory;

  public static void Use(UseServiceDelegate<T> codeBlock)
  {
    if (_channelFactory == null)
      _channelFactory = new ChannelFactory<T>("AlyzaServiceEndpoint");
    IClientChannel proxy = (IClientChannel)_channelFactory.CreateChannel();
    bool success = false;
    try
    {
      codeBlock((T)proxy);
      proxy.Close();
      success = true;
    }
    finally
    {
      if (!success)
      {
        proxy.Abort();
      }
    }
  }
}

Comme indiqué dans ma question, voici comment cette classe est utilisée dans mon ViewModel:

Service<Sam.Alyza.WcfInterface.IServiceWebsites>.Use(alyzaSvc =>
{
  rc = new List<Sam.Alyza.WcfInterface.Website>(alyzaSvc.GetSites());
});

Pour simuler l'interface WCF, il me faudrait créer et héberger un service simulé WCF, modifier toutes les chaînes de connexion. Beaucoup de travail juste pour ajouter quelques tests.

J'ai trouvé un moyen plus simple: Il est simple de créer un service fictif implémentant l'interface:

public class MockWebsiteService : WcfInterface.IServiceWebsites
{
  internal List<Sam.Alyza.WcfInterface.Website> _websites = new List<Sam.Alyza.WcfInterface.Website>();
  internal int _GetSitesCallCount;

  IEnumerable<Sam.Alyza.WcfInterface.Website> Sam.Alyza.WcfInterface.IServiceWebsites.GetSites()
  {
    _GetSitesCallCount++;
    return _websites;
  }
}

Le seul problème est: comment faire en sorte que ViewModel appelle cette classe fictive au lieu du service?
Une solution: Service.Use () gère la connexion. En ajoutant des fonctionnalités permettant de remplacer la gestion des connexions, je peux insérer mon propre objet fictif WCF dans Service.Use ().
Pour cela, j'ai besoin d'un moyen de faire en sorte que Service.Use () appelle autre chose que WCF (consultez les sections #DEBUG):

public static class Service<T>
{
#if DEBUG
  public static T DebugOverride = default(T);
#endif
  public static ChannelFactory<T> _channelFactory;

  public static void Use(UseServiceDelegate<T> codeBlock)
  {
#if DEBUG
    if (!Object.Equals(DebugOverride, default(T)))
    {
      codeBlock(DebugOverride);
      return;
    }
#endif
    if (_channelFactory == null)
      _channelFactory = new ChannelFactory<T>("AlyzaServiceEndpoint");
    IClientChannel proxy = (IClientChannel)_channelFactory.CreateChannel();
    bool success = false;
    try
    {
      codeBlock((T)proxy);
      proxy.Close();
      success = true;
    }
    finally
    {
      if (!success)
      {
        proxy.Abort();
      }
    }
  }
}

En ajoutant ce crochet de test dans Service, je peux insérer n'importe quel objet implémentant T dans mes tests:

MockWebsiteService mockmodel = new MockWebsiteService();
Service<WcfInterface.IServiceWebsites>.DebugOverride = mockmodel;
// run my tests here

Pour moi, c’est un très bon moyen de se moquer d’un service WCF!

PS: Je suis conscient qu'en raison de #if DEBUG, les tests ne seront pas compilés dans la version. Il suffit de les expulser si cela vous va.

Autres conseils

Nous utilisons l’injection de dépendance pour traiter ce problème, en injectant le client du service (ou peut-être une usine pour les clients du service) dans le ViewModel. Quelque chose comme ça:

interface IClientFactory
{
    TClient CreateClient<TClient>();
}

class ClientFactory : IClientFactory
{
    TClient CreateClient<TClient>() 
    {
       var channelFactory = new ChannelFactory<TClient>("AlyzaServiceEndpoint");
       var proxy = (TClient)channelFactory.CreateChannel();
       return proxy;
    }
}

public ViewModel 
{
    public ViewModel(IClientFactory clientFactory)
    {
       _clientFactory = clientFactory;
    }

    private void DoWcfStuff()
    {
        using (var proxy = _clientFactory.CreateClient<IClientChannel>())
        {
           var result = proxy.GetThings();
        }
    }
}

public ViewModelTests
{
    public void Setup()
    {
       _mockFactory = new MockClientFactory();
       _viewModel = new ViewModel(_mockFactory);
    }

    [Test]
    public void Test() 
    {
       var testResult = new Result();
       var mockClient = _mockFactory.CreateClient<IClientChannel>();

       mockClient.SetResultForGetThings(testResult);

       // put the viewmodel through its paces.
    }

    private class MockClientFactory : IClientFactory
    {
        MockClient _mockClient;

        public MockClientFactory()
        {
          _mockClient = new MockClient();
        }

        public TClient CreateClient<TClient>()
        {
           if (typeof(TClient) == typeof(IClientChannel))
           {
              return _mockClient;
           }
        }
    }

    private class MockClient : IClientChannel
    {
        void SetupGetThingsResult(Result result)
        {
           _result = result;
        }

        Result GetThings() 
        {
           return _result;
        }
    }
}

J'ai montré un exemple en utilisant des simulacres à code manuel. J'utilise généralement un framework Mocking tel que Moq .

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top