22. Januar 2014 · von Steffen Nörtershäuser

Erklärt: CRM 2013 Unit Tests

Unit Tests helfen stark dabei, die einzelnen Komponenten eines Softwareprogramms zu testen. Sie helfen dabei Fehler in einzelnen Modulen frühzeitig aufzudecken und verhindern so größere Testaufwände für einen ausführlichen Integrationstest. Auf diese Weise kann eine weitestgehend fehlerfreie Software mit verhältnismäßig geringem Aufwand erzeugt werden.

Wichtig ist dabei, dass die einzelnen Komponenten isoliert getestet werden. Auf diese Weise kann sichergestellt werden das ein auftretender Fehler innerhalb der Komponente zu finden ist und nicht durch ein externes System (wie ein Webservice) verursacht wird.

Solche Bedingungen sind für Dynamics CRM 2013 Plugins schwer herzustellen da sie nicht ohne weiteres außerhalb des CRM Kontext ausführbar sind und somit immer abhängig vom CRM System. Auch ist es ohne weitere Arbeit nicht möglich CRM Plugins automatisiert auszuführen und so Unit Tests schnell wiederholbar zu machen.

Wie diese Problem jedoch gelöst werden können und auch für CRM 2013 Plugins isolierte Unit Tests umsetzbar sind wird in diesem Artikel beschrieben. Als Beispiel wird dafür ein Plugin implementiert welches beim Anlegen einer Firma für eine Bankleitzahl den dazugehörigen Banknamen ermittelt.

Anlegen des Plugin Rahmen
Zu Beginn wird lediglich der Rahmen des Plugins angelegt. Hierauf werde ich nicht näher eingehen da dies nicht der Fokus dieses Artikels ist. Die Daten des Plugins sind jedoch:
blz_plugin_data
Bevor das Plugin mit Programmcode gefüllt wird, wird der Rahmen des Unit Tests angelegt.

CRM 2011 Plugin Testing Tools
Hierbei helfen die CRM 2011 Plugin Testing Tools. Diese können mit geringem Aufwand auch für CRM 2013 genutzt werden. Hierfür muss lediglich der SourceCode heruntergeladen werden und die Verweise auf das Microsoft.Xrm.Sdk auf die Version der DLL aus dem CRM SDK 2013 aktualisiert werden.

Zusätzlich muss ein kleiner Fehler in den CRM 2011 Plugin Testing Tools behoben werden. Innerhalb der Datei TestPlugin/MyServiceProvider.cs existiert standardmäßig folgende Zeile im Konstruktor:

           //Deserialize the context object found at the MyServiceProvider's file-path to create a context object for testing
           if (filepath != null) { MyPluginContext myPluginContext = PluginTester.DeSerializePluginContext(filepath); }

Wie hier zu sehen ist wird der PluginContext nicht korrekt an die Membervariable „context“ des ServiceProviders zugewiesen. Aus diesem Grund würde der ServiceProvider in diesem Zustand keinen PluginExecutionContext zurückgeben und somit alle Tests fehlschlagen. Daher muss die Zeile wie folgt geändert werden:

           //Deserialize the context object found at the MyServiceProvider's file-path to create a context object for testing
           if (filepath != null) { this.context = PluginTester.DeSerializePluginContext(filepath); }

Nachdem die DLLs anschließend neu gebauten wurden sind sie bereit für den Einsatz mit CRM 2013.

Sammeln von Testdaten
Um nun Testdaten für den Unit Test zu sammeln müssen Plugin Informationen wie Entität, Nachricht und Pipeline Stage bekannt sein. Sind diese Informationen bekannt können die Testdaten für den Unit Test gesammelt werden. Dafür muss die neu erstellte SerializePluginContext.dll über das PluginRegistrationTool in CRM registriert werden:register_serializecontext

Anschließend muss für dieses Plugin ein Step registriert werden. Hierbei müssen wie oben erwähnt die Daten des zu testenden Plugins übernommen werden. In unserem Beispiel wird für die Entität „Account“ ein Plugin für die Pre-Operation Create Nachricht getestet:
register_serializecontext_step

Nachdem dieser Schritt registriert wurden ist wechselt man in das CRM System und legt einen neuen Eintrag für die betroffene Entität an. Da ein Plugin implementiert wird welches eine Bankleitzahl benötigt müssen vor dem Speichern die Pflichtfelder und das Bankleitzahl Feld ausgefüllt werden:
create_new_account

Wird der Account nun wie gewohnt gespeichert legt das vorher registrierte Plugin unter C:WindowsTemp eine Datei mit allen für den Test notwendigen Daten an. Diese heißt {Entitätsname}-{Message}-{Laufende Nummer}.xml. In unserem Beispiel also „account-Create-001.xml“.
Nun sind alle notwendigen Daten für den Test gesammelt und es kann mit dem nächsten Schritt weitergehen.

Anlegen des UnitTest
Für den UnitTest muss ein VisualStudio Unit Test Projekt hinzugefügt werden. Diese Projekt wird in die CRM Plugin Lösung eingefügt:

create_unit_test_project

Den bereits existierenden UnitTest1 benenne ich nun in PreAccountCreateGetBankNameTest um. Durch eine solche Benamung ist schnell klar das es sich bei diesem Test um einen Test für das PreAccountCreateGetBankName-Plugin handelt. Die Test Methode TestMethod1 wird in „GetBankNameTest“ umbenannt da es in unserem ersten Test nur geprüft wird ob bei einer korrekten Bankleitzahl ein korrekter Bankname ermittelt wird.
Nun muss die XML-Datei mit vorher gesammelten Testdaten angefügt werden. Hierfür lege ich einen Ordner „TestFiles“ innerhalb des Unit Tests an. Anschließend wird die XML-Datei wie folgt über Rechtsklick auf den Ordner hinzugefügt:
add_testdata_xml

Für diese Datei muss nun eingestellt werden das sie in das Zielverzeichnis kopiert wird um für den Test zu Verfügung zu stehen. Dafür wählt man die Eigenschaften der Datei aus und wählt unter dem Punkt „Copy to Output Directory“ „Copy always“ aus.

Anschließend kann die Testmethode mit Leben gefüllt werden. Hier die fertige Methode. Ich werde im Anschluss auf die Details eingehen:

          
        /// <summary>
        /// Prüft ob der Bankname bei einer korrekten Bankleitzahl korrekt ermittelt wird
        /// </summary>
        [TestMethod]
        public void GetBankNameTest()
        {
            // Arrange
            const string filename = "TestFiles/account-Create-001.xml";

            MyServiceProvider serviceProvider = new MyServiceProvider(filename);
            PreAccountCreateGetBankName plugin = new PreAccountCreateGetBankName();

            // Act
            plugin.Execute(serviceProvider);
            Entity targetEntity = plugin.PluginContext.PluginExecutionContext.InputParameters["Target"] as Entity;
            Account account = targetEntity.ToEntity();

            // Assert
            Assert.AreEqual("Postbank (Giro)", account.g4_bankname);
        }

In der Arrange Phase wird ein Fake CRM-ServiceProvider aus der XML-Datei erzeugt sowie eine Instanz der Plugin Klasse. Der Fake CRM-ServiceProvider lädt dabei die Daten welche wir innerhalb des Account Formulars im vorherigen Schritt erzeugt haben.
In der darauffolgenden Act Phase wird nun das eigentliche Plugin aufgerufen. Der Fake CRM-ServiceProvider verhält sich dabei so wie der ServiceProvider zu dem Zeitpunkt als wir im Schritt „Sammeln von Testdaten“ die XML-Datei erzeugt haben.
Anschließend werden die Ergebnisse des Plugins abgefragt und in der Assert Phase geprüft.
Wenn man versucht diesen Unit Test so zu kompilieren wird man auf Fehler stoßen. Es fehlen einige Verweise innerhalb des Unit Test Projekt. Zum einen muss ein Verweis auf das Plugin Projekt innerhalb der Solution hinzugefügt werden um eine neue Instanz der PreAccountCreateGetBankName Klasse zu erzeugen können. Desweiteren muss ein Verweis auf die TestPlugin.dll des angepassten CRM 2011 Plugin Testing Tools eingefügt werden. Um die Entitätsdaten anschließend abzufragen muss ein Verweis auf die CRM-DLL Microsoft.Xrm.Sdk.dll sowie auf System.Runtime.Serialization eingefügt werden.
Nun sind alle notwendigen Verweise vorhanden, jedoch muss die Plugin.cs Datei des CRM Plugin Templates etwas angepasst werden um die Ergebnis Daten abfragen zu können. Zu Beginn muss die protected Klasse LocalPluginContext innerhalb der Plugin Klasse auf public geändert werden sowie ihr Member PluginExecutionContext. Innerhalb des Konstruktors der LocalPluginContext Klasse muss geprüft werden ob der serviceProvider eine Instanz der IOrganizationServiceFactory zurück liefert, da dies nicht immer der Fall bei den CRM 2011 Plugin Testing Tools ist. Der neue Konstruktor sieht wie folgt aus:

            internal LocalPluginContext(IServiceProvider serviceProvider)
            {
                if (serviceProvider == null)
                {
                    throw new ArgumentNullException("serviceProvider");
                }

                // Obtain the execution context service from the service provider.
                this.PluginExecutionContext = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

                // Obtain the tracing service from the service provider.
                this.TracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));

                // Obtain the Organization Service factory service from the service provider
                IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

                // Use the factory to generate the Organization Service. Modified for UnitTest
                if (factory != null)
                {
                    this.OrganizationService = factory.CreateOrganizationService(this.PluginExecutionContext.UserId);
                }
            }

Zu guter letzt muss eine neue public Property PluginContext vom Typ LocalPluginContext eingefügt werden:

        public LocalPluginContext PluginContext
        {
            get;
            private set;
        }

Innerhalb der Execute Methode der Plugin Klasse muss nun keine lokale Variable mehr als LocalPluginContext angelegt werden, sondern diese Property befüllt werden und alle Verweise auf die lokale Variable auf diese Property geändert werden. Beispielsweise:

            // Construct the Local plug-in context.
            PluginContext = new LocalPluginContext(serviceProvider);

            PluginContext.Trace(string.Format(CultureInfo.InvariantCulture, "Entered {0}.Execute()", this.ChildClassName));

Hier lässt sich eine geänderte Plugin.cs Datei als Textdatei herunterladen.

Entwickeln des Plugins
Nun kann das eigentliche Plugin implementiert werden. Der Vorteil bei dieser Vorgehensweise ist es, dass das Plugin nicht für jeden Test in den CRM-Server deployed werden muss sondern die Entwicklung über den Unit Test erfolgen kann. Dadurch wird Zeit gespart und der CRM-Server wird weniger belastet. Nachdem der Unit Test für das Plugin mit allen zugehörigen Test erfolgreich durchgelaufen ist kann das Plugin in den Server deployed werden.
Hier nun der fertige Programmcode des Plugins:

        protected void ExecutePreAccountCreateGetBankName(LocalPluginContext localContext)
        {
            if (localContext == null)
            {
                throw new ArgumentNullException("localContext");
            }

            // Daten abfragen
            Entity targetEntity = localContext.PluginExecutionContext.InputParameters["Target"] as Entity;
            Account account = targetEntity.ToEntity();

            // Webservice erzeugen und abfragen
            BasicHttpBinding binding = new BasicHttpBinding();
            binding.Security.Mode = BasicHttpSecurityMode.None;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
            binding.Security.Transport.ProxyCredentialType = HttpProxyCredentialType.None;
            binding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName;

            EndpointAddress endpointAddress = new EndpointAddress("http://www.thomas-bayer.com/axis2/services/BLZService");

            BLZServicePortTypeClient client = new BLZServicePortTypeClient(binding, endpointAddress);

            detailsType type = client.getBank(account.g4_bankcode);

            // Daten zuweisen
            account.g4_bankname = type.bezeichnung;
        }

Der Code fragt einen Webservice unter der URL http://www.thomas-bayer.com/axis2/services/BLZService ab. Dieser wurde zuvor als Service Reference in das Plugin Projekt eingefügt und liefert den Namen einer Bank anhand der Bankleitzahl. Näher möchte ich nicht auf das Plugin eingehen. Erwähnenswert ist höchstens noch das innerhalb von CRM Plugins die Bindings für Webservices im Code erzeugt werden müssen und nicht über die Config definierbar sind.

Fazit
Führt man nun den Unit Test aus wird sich zeigen das dieser erfolgreich durchläuft. Dabei wird an keiner Stelle der CRM-Server kontaktiert und der Unit Test ist zu mindestens was den CRM Server angeht isoliert und automatisiert durchführbar. Bei der Entwicklung des Plugins wurde keinmal der CRM-Server kontaktiert und die Iterationszeit zwischen Entwicklung und Test wurde gering gehalten.

In dieser Hinsicht wurden also unsere Anforderungen an einen Unit Test umgesetzt. Vollständig isoliert ist dieser Unit Test jedoch noch nicht. Wäre der Bank Webservice nicht erreichbar oder ein Fehler innerhalb dieses Webservice würde der Unit Test auf einen Fehler laufen ohne das unser Code daran Schuld wäre. Wie man den Unit Test auch im Bezug auf Webservices isoliert werde ich in einem bald folgenden Beitrag erklären.



Diesen Blogeintrag bewerten:


Haben Sie Fragen zu diesem Artikel oder brauchen Sie Unterstützung?

Nehmen Sie mit uns Kontakt auf!

Wir unterstützen Sie gerne bei Ihren CRM-Vorhaben!


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Kontakt
Lassen Sie sich von uns beraten
Wir freuen uns über Ihr Interesse an unseren Leistungen. Hinterlassen Sie
uns Ihren Namen, Ihre Telefonnummer und E-Mail Adresse – wir melden
uns schnellstmöglich bei Ihnen.
Kontakt aufnehmen