SPEmulators auf NuGet verfügbar

Eine lange Zeit nun warten die SharePoint-Entwickler auf eine neue Version der Microsoft.SharePoint.Emulators für SharePoint 2013. Leider scheint Microsoft aber in absehbarer Zukunft keine neue Version herauszubringen. Das ist völlig unverständlich, da es ja auch viele 2010er Lösungen gibt, die nach 2013 migriert werden müssen. Um die Lücke zu schließen gibt es jetzt das Nuget-Package SPEmulators. Der Code ist auf GitHub verfügbar.

Installation

Die Installation erfolgt wie immer per Package-Manager-Console mit dem Befehl: “Install-Package SPEmulators”. Alternativ kann über “Manage NuGet Packages” nach dem Paket gesucht werden. Ein Klick auf “Install” fügt das Paket dem aktuelle Projekt hinzu.

image

Erstellen des ersten Tests

Wie auch bei den den Microsoft.SharePoint.Emulators werden die die SPEmulators über einen Context – den SPEmulationContext – initialisiert. Der Konstruktor übernimmt als Argument das “IsolationLevel”.

using (var context = new SPEmulationContext(IsolationLevel.Fake))
{
    //...
}

IsolationLevel

Im unterschied zu den Microsoft.SharePoint.Emulators gibt es hier 3 unterschiedliche Level:

IsolationLevel Beschreibung
Fake Mit dem IsolationLevel.Fake werden alle Aufrufe zu SharePoint angefangen und gegen Fake-Objekt ausgeführt. Die Tests sind deshalb sehr schnell und eigenen sich für die Integration in den normalen Buildprozess. Wird im Test auf Objekt wie Listen oder ListItems zugegriffen, dann müssen diese erst den “gefakten” hinzugefügt werden.
Entspricht dem EmulationMode.Enabled.
Integration Mit dem IsolationLevel.Integration werden die Tests gegen den lokalen SharePoint-Server ausgeführt. Die URL, die im Konstruktor übergeben werden kann muss dafür auf dem System existieren. Die SPSite und das SPWeb der URL werden über Fakes in SPContext eingefügt und sind auch darüber erreichbar.  Listen oder ListItems müssen vor den Tests “deployed” werden. Die Tests sind viel langsamer und eigenen sich für Integrationstests in der Continuous-Deployment-Pipeline.
None Im IsolationLevel.None findet keine Isolierung statt. Das SPSite- und SPWeb-Objekt werden auch nicht dem SPContext injiziert. Ebenfalls wird kein ShimsContext erstellt. Dieser Mode ist hauptsächlich für die Migration bestehender Lösungen gedacht. Er entspricht dem EmulationMode.Passthrough.

Es gibt einen zweiten Konstruktor, dem neben dem IsolationLevel auch eine URL übergeben werden kann. Dies ist die bevorzugte Methode für neue Projekte um Tests sowohl als Unit- als auch als Integrationstests verwenden zu können.

using (var context = new SPEmulationContext(IsolationLevel.Fake, "http://localhost/"))
{
    var web = context.Web;
    var site = context.Site;
	
    // ...
}

Wie im Snippet zu sehen, hat der SPEmulationContext Eigenschaften, um auf das aktuelle Web oder die aktuelle Site zugreifen zu können. Ebenfalls hat er eine Methode GetOrCreateList, die abhängig vom IsolationLevel eine Liste lädt oder sie vorher dem Fake-Web hinzufügt. Die Funktion übernimmt dabei neben dem Namen und dem Typ der Liste auch optional eine Array von Spaltennamen, die ggf. der Liste im Fake-Mode hinzugefügt werden. Die Funktion deckt sicher nicht 100% der Anforderungen in dem Bereich ab. Es steht ja aber jedem frei, die Liste selber dem Fake-Web mit den richtigen Spalten hinzuzufüge,

var list = context.GetOrCreateList("ListName", SPListTemplateType.GenericList, "Fieldd1", "Field2", "Field3");

Performance

Bei der Performance spiel es natürlich eine große Rolle, ob das IsolationLevel Fake oder Integration gewählt wird. Derselbe Test dauert entweder im Millisekundenbereich oder etliche Sekunden.

image

 

Best Practices

Wer die selben Tests sowohl als Unit- als auch als Integrationstests verwenden möchte, der kann das IsolationLevel und die URL in den AppSettings hinterlegen. Dadurch kann beim Build über das Deployment der entsprechenden app.config die Art der Tests und ggf. die URL konfiguriert werden.

image

Die Tests

Ansonsten unterscheidet sich das vorgehen nicht von dem bisherigen. Es sei an dieser Stelle deshalb für Neulinge im Bereich TDD für SharePoint auf die Dokumentation der Microsoft.SharePoint.Emulators verwiesen. In der Dotnetpro gibt es z.B. einen ausführlichen Artikel von mir mit Beispielcode. Ansonsten gib es noch auf MSDN einen Artikel und ebenfalls die offizielle Dokumentation.

Hier noch ein längeres Beispiel, wie eine Testklasse z.B. aussehen könnte.

[TestClass]
public class CustomerListRepositoryTest
{
    SPEmulationContext context;
    SPList customerList;
    CustomerListRepository repository;

    [TestInitialize]
    public void TestInitialize()
    {
        context = new SPEmulationContext(Settings.Default.IsolationLevel, Settings.Default.Url);
        customerList = context.GetOrCreateList(CustomerListRepository.ListName, SPListTemplateType.GenericList);
        repository = new CustomerListRepository();
    }

    [TestMethod]
    public void CanAddCustomer()
    {
        var itemCount = customerList.ItemCount;
        repository.Add(new Customer { Name = "Customer 3" });

        Assert.AreEqual(itemCount + 1, customerList.Items.Count);
    }

    [TestMethod]
    public void CanGetCustomerByName()
    {
        if (context.IsolationLevel == IsolationLevel.Fake)
        {
            var item2 = this.customerList.Items.Add();
            item2["Title"] = "Customer 1";
            item2.Update();

            new ShimSPList(this.customerList)
            {
                GetItemsSPQuery = (q) =>
                {
                    var shim = new ShimSPListItemCollection();
                    shim.Bind(new[] { item2 });
                    return shim.Instance;
                }
            };
        }
        
        var result = repository.GetByName("Customer 1");
        Assert.IsNotNull(result);
    }

    [TestMethod]
    public void GetCustomerByNameReturnsNull()
    {
        if (context.IsolationLevel == IsolationLevel.Fake)
        {
            var item = this.customerList.Items.Add();
            item["Title"] = "Customer 1";
            item.Update();

            new ShimSPList(customerList)
            {
                GetItemsSPQuery = (q) =>
                {
                    var shim = new ShimSPListItemCollection();
                    shim.Bind(new SPListItem[0]);
                    return shim.Instance;
                }
            };
        }

        var result = repository.GetByName("Customer XXX");
        Assert.IsNull(result);
    }

    [TestMethod]
    public void CanGetCustomerById()
    {
        if (context.IsolationLevel == IsolationLevel.Fake)
        {
            var item = this.customerList.Items.Add();
            item["Title"] = "Customer 1";
            item.Update();
        }

        var result = repository.GetById(1);
        Assert.IsNotNull(result);
        Assert.AreEqual("Customer 1", result.Name);
    }

    [TestMethod]
    public void GetCustomerByIdReturnsNull()
    {
        var repository = new CustomerListRepository();
        var result = repository.GetById(10);
        Assert.IsNull(result);
    }

    [TestMethod]
    public void CanGetListOfCustomers()
    {
        if (context.IsolationLevel == IsolationLevel.Fake)
        {
            var item = this.customerList.Items.Add();
            item["Title"] = "Customer 1";
            item.Update();

            var item2 = this.customerList.Items.Add();
            item2["Title"] = "Customer 1";
            item2.Update();

            new ShimSPList(customerList)
            {
                GetItemsSPQuery = (q) =>
                {
                    var shim = new ShimSPListItemCollection();
                    shim.Bind(new[] { item, item2 });
                    return shim.Instance;
                }
            };
        }

        var result = repository.GetAll();
        Assert.AreEqual(2, result.Count());
    }

    [TestCleanup]
    public void TestCleanUp()
    {
        if (this.context.IsolationLevel != IsolationLevel.Fake)
        {
            var startId = 3; // if you have items deployed by default set start id to appropriate value
            var itemsToClear = from i in customerList.Items.Cast<SPListItem>()
                                where i.ID >= startId
                                select i.ID;

            foreach (var id in itemsToClear)
                customerList.Items.DeleteItemById(id);
        }

        context.Dispose();
    }
}

Fazit

JavaScript und das neue App-Model werden in der Entwicklung für SharePoint immer wichtiger. Trotzdem werden die Farm-Solutions in absehbarer zeit nicht ganz verschwinden. Und in den  Farm-Solutions ist die Qualität besonders wichtig. Die #SPEmulators sind eine große Hilfe auch für #SP2013 qualitative hochwertigen und durch eine hohe Testabdeckung abgesicherten Code zu entwickeln.

NuGet: https://www.nuget.org/packages/SPEmulators/ 

Repository: https://github.com/wulfland/SPEmulators

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s