Your Docker container takes care of persisting your SQL Server database

Microsoft has a great solution for persisting your local data collected by the IoT Edge. It’s up to you to get the data in and out of your database. But you have a convenient way of storing your precious information into some kind of persistent storage.

As seen in my previous blog, it’s not that hard to deploy from the cloud and administer your SQL database locally.

But how persistent is your database actually? Can you trust Docker for taking care of your data?

In this blog, we try to answer this question.

Doorgaan met het lezen van “Your Docker container takes care of persisting your SQL Server database”

Advertenties

Administer your SQL Server in Docker

As with many things, you have to do it, to believe it.

The same goes for the Azure IoT Edge solution.

With the new IoT Edge solution, Microsoft provides a platform, both powerful and scalable. It’s based on Docker and logic is put in Docker containers.

And in many presentations, this (correct) picture is used to show the capabilities:

I am already working with the IoT Edge preview quite some time and there is a ‘weak’ spot in the image.

I already marked it in red so it’s not that hard to find.

On several occasions, non-technical people explained the local storage as being a database to persist data from internal logic.

I understand the confusion, but this is just a ‘database’ used by the IoT Edge internals (I assume mostly the Edge Hub module) and it is not accessible by users. I know for sure it’s used for the store-and-forward pattern used to send ‘upstream’ messages to the cloud.

Once a message is routed to be sent to the cloud, it’s first stored by the EdgeHub. If the connection to the Azure IoT Hub cannot be established, the message is stored with a certain retention time ( see its configuration “storeAndForwardConfiguration”: { “timeToLiveSecs”: 7200} ).

So, how can we add local storage to our IoT Edge if we want to do something with custom code and databases, etc.

Well, Microsoft already has written a great piece of documentation here.

There, you can see how you create an SQL Server database both on Linux and Windows containers and you learn how to create a database and a table inside it. Finally, you access it using some Azure function.

Let’s dig a bit deeper into this.

We will look on other  (simpler) ways to work with the database.

Doorgaan met het lezen van “Administer your SQL Server in Docker”

Passing JSON arrays to SqlAzure from Azure Functions using OPENJSON

During my last project, we had to pass arrays of data to SqlAzure.

Normally, passing data is not that interesting. But we were passing data in bulk to SqlAzure so there is always a chance of throttling.

This is when you pass correct records towards SqlAzure but you reach certain limitations eg. if you hit the DTU limits.

We were getting a few hundred records which we first inserted one by one (using the ExternalTable (experimental) output). But we only we able to insert approx. forty records.

So we switched back to a stored procedure.

The second challenge was how to pass an array of records. Because we got the data as JSON, we wanted to pass an array of records like JSON.

In the example below, I show how to call a stored procedure within an Azure Function. The code could be simplified if not I wanted to do some transformations.

Doorgaan met het lezen van “Passing JSON arrays to SqlAzure from Azure Functions using OPENJSON”

T-Sql money verhonderdvoudigt kosten in Azure

Seeing anything you like? Google translate does not work out? Drop me a note and I will translate this post.

Met Sql Server 2012 Management Studio is het nu eenvoudig om bacpac-bestanden te maken om zo Sql Server databases te exporteren en te importeren. Dit is dé manier om Sql-Azure databases initieel mee te vullen.

Een bacpac bestaat uit een combinatie van: de scripts om de database aan te maken en json bestanden om de te exporteren data in op te slaan. En deze is in zijn geheel gezipt om de overdracht te verspoedigen. (een bacpac stelt ook eisen aan de meta data overigens, de database moet eerst door een validatie heen.)

Wij gebruiken op ons project bacpac-bestanden om onze lokale database dus naar de cloud te brengen.

Gisteren kreeg ik de melding van onze tester dat de prijzen van artikelen, opgehaald uit Azure, honderd maal zo groot waren vergeleken met een lokale database. Ik kon het inderdaad naspelen en kwam er op een gegeven moment achter dat het gewoon aan de vulling van de database lag. De prijzen waren in de Sql-Azure databasetabel gewoon honderd maal hoger.

Het veld was als money in TSql opgenomen. Dit zal historisch ooit in de database geslopen zijn. Persoonlijk vind ik een decimal voldoende. Mocht ik het type valuta (anders dan euro’s) willen opslaan dan zou ik de valuta isocode daarvan in een andere kolom opnemen. En kan een money veld ook met vier cijfers achter de komma omgaan, iets wat veel banken doen om (zichtbare) afrondingsfouten te voorkomen?

Ik heb toen gekeken naar waar dit aan zou kunnen liggen en ik kwam bij de bacpac uit. We hadden nog de originele versie en deze kan je simpel openen door de extensie met .zip te verlengen. Dan kom je ook bij de json bestanden uit en daar stond het: de prijzen waren als decimaal afgebeeld in de json waarden voor de bedragen maar er zat een komma in!

Nu zijn wij Nederlanders en mijn collega had zijn lokale instellingen op Dutch gezet. Op zijn machine is de bacpac aangemaakt. Zou het daar aan liggen, zou de locale van zijn machine een rol spelen? We hebben dus een testje gedaan.

We maakten een tabel met de volgende velden:

[Id] [bigint] IDENTITY(1,1) NOT NULL,[Name] [nvarchar](50) NULL,

[Age] [int] NULL,

[Decimal] [decimal](5, 3) NULL,

[Float] [float] NULL,

[Money] [money] NULL,

[Numeric] [numeric](10, 5) NULL,

[Datetime] [datetime] NULL,

[Datetimeoffset] [datetimeoffset](7) NULL,

Als je deze exporteert met een Engelse locale krijg je:

{“0″:[1,”test   name”,23,”3.566″,”3.566″,”3.55″,”3.56600″,”2012-12-23T21:45:59.0000000″,”2012-12-23T23:45:31.0000000+01:00″]}

Als je deze exporteert met een Nederlandse locale krijg je:

{“0″:[1,”test   name”,23,”3.566″,”3.566″,”3,55″,”3.56600″,”2012-12-23T21:45:59.0000000″,”2012-12-23T23:45:31.0000000+01:00″]}

Het money veld volgt dus inderdaad de locale. En ja, onze databaseprijzen waren dus ingesteld met dat money veld.

Blijkbaar beschouwt Azure, of de conversie er naar toe, 3,55 euro als 355 euro. De decimale komma gaat op in een ‘formattering’ correctie.

Conclusie: klanten van Microsoft die niet met de Engelse locale werken kunnen beter geen money veld toepassen in Sql-Azure. De decimal is een waardig alternatief tot het moment waarop Microsoft besluit deze ‘feature’ te verhelpen.

Stored procedures ontsluiten met OData service operations

OData is een heel interessant communicatie middel om informatie uit te
wisselen. De grootste vernieuwing is dat de client, niet de server, uitmaakt
welke data over het lijntje gaat. Waar ‘klassieke’ webservices een keur aan
methodes beschikbaar stellen om alle mogelijke selecties te ondersteunen, heeft
OData voldoende aan het ontsluiten van een context met entiteiten.

Maar zoals altijd moeten nieuwe technieken aansluiten op al bestaande
oplossingen. Voor OData geldt hetzelfde. In deze blog laat ik zien hoe OData
gebruikt kan worden om stored procedures te ontsluiten.

OData ontsluiten

Hoewel inhoudelijk niet zo relevant, laat ik eerst even het model van
entiteiten die al door een OData service ontsloten kunnen worden.

datamodel

Deze entiteiten zijn een directe afgeleide van drie tabellen in de
database. Maar in diezelfde database is ook een stored procedure beschikbaar
welke de machines toont die een minimum aantal storingen
hebben.


CREATE PROCEDURE USP_Machines_With_Minimum_Amount_Of_Failures

@MinimumCount int = 99

AS

BEGIN

SELECT M.MachineId, M.MachineName, M.SupervisorName

FROM Machines M

WHERE @MinimumCount <=

( SELECT COUNT(P.PartId)

FROM Parts P

JOIN Failures F ON P.MachineId = M.MachineId

AND P.PartId = F.PartId )

END

Merk op dat we als resultaat een lijst van records teruggeven waarvan
de opmaak niet 1-op-1 overeenkomt met de Machine entiteit. We missen enkele kolommen.

Voor de te bouwen WCF Data Service maken we hier gebruik van een
Entity Framework model. We voegen dus een EDMX toe aan een webproject en geven aan het model de opdracht om de drie tabellen en de stored procedure te gaan representeren.

EDMX

De stored procedure moet hierbij opnomen zijn om de volgende stap
te kunnen maken. Stored procedures die niet aan het model kenbaar zijn gemaakt in deze stap, kunnen later ook niet geselecteerd worden.

Als we de model-browser openen dan zien we ook netjes dat de stored
procedure opgenomen is:

Model browser

Maar we zijn hiermee nog niet klaar met het model. De stored procedure is nu
nog niet beschikbaar in de context. We moeten nu een function import uitvoeren
zodat de context de stored procedure in entiteiten kan uitdrukken.  Een
rechtermuis klik op de EDMX moet voldoende zijn om de function import te
starten:

Function import

Dit geeft een wizard waarbij een aantal zaken ingesteld moeten worden.

Function import wizard

Eerst geven we bovenaan een nettere naam aan de functie. En we kunnen onze
stored procedure selecteren (..)

Vervolgens moeten we de te retourneren waarden vaststellen en hierbij
moeten we goed opletten. Omdat de stored procedure geen complete entiteiten
retourneert, gaan we een nieuw complex type genereren.

Druk hiervoor op de “Get Column Information” knop. De te retourneren
waarden worden vastgesteld uit de stored procedure . Druk vervolgens op de knop “Create new complex type”:

Wizard part 2

En kies als laatste in het midden van het scherm voor het retourneren van
een lijst van het nieuw gegenereerde complex type (te herkennen aan de naam met _Result achter de stored procedure naam).

Function imported

Let op: Het return type moet bij de eigenschappen ingesteld zijn!

Nu is de stored procedure als functie beschikbaar in de context. We gaan dus
over op het aanmaken van OData service.

De volgende stap is dus het toevoegen  van een WCF Data service.

public class WcfDataService1 : DataService<SdnODataEntities>
{
  public static void InitializeService(DataServiceConfiguration config)
  {
    config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
    config.SetEntitySetPageSize("*", 10);
    config.DataServiceBehavior.MaxProtocolVersion =
      DataServiceProtocolVersion.V2;
  }
}

Als we deze service controleren op het bestaan van het nieuwe complex type,
dan blijkt dat er nog niks is veranderd, voor de service zelf. Alleen de drie
entiteiten zijn nog steeds direct te benaderen:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<service xml:base="htt p://localhost:2239/WcfDataService1.svc/"
    xmlns:atom="htt p://www.w3.org/2005/Atom"
    xmlns:app="htt p://www.w3.org/2007/app"
    xmlns="htt p://www.w3.org/2007/app">
  <workspace>
    <atom:title>Default</atom:title>
    <collection href="Failures">
      <atom:title>Failures</atom:title>
    </collection>
    <collection href="Machines">
      <atom:title>Machines</atom:title>
    </collection>
    <collection href="Parts">
      <atom:title>Parts</atom:title>
    </collection>
  </workspace>
</service>

Het nieuwe type is dus niet te kiezen en zal ook nooit zo direct
beschikbaar zijn. En ook via de metadata is het type (nog) niet
controleerbaar.

Maar de stored procedure is wel beschikbaar binnen de context en deze gaan we
als Service Operation ontsluiten:

public static void InitializeService(
DataServiceConfiguration config)
{
  config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
  config.SetEntitySetPageSize("*", 10);
  config.DataServiceBehavior.MaxProtocolVersion
     = DataServiceProtocolVersion.V2;
  config.SetServiceOperationAccessRule(
     "GetMachinesWithMinimumCountOfFailures",
     ServiceOperationRights.AllRead);
}

[WebGet]
public List<MachinesWithMinimumCountOfFailures_Result>
        GetMachinesWithMinimumCountOfFailures(int minimumCount)
{
  return this.CurrentDataSource.
        MachinesWithMinimumCountOfFailures(minimumCount).ToList();
}

We maken dus een methode aan die met een WebGet attribuut opgemaakt is.
En deze service operation is voor alle leesacties beschikbaar.

Als we nu de OData service starten en de metadata controleren (http://localhost:2239/WcfDataService1.svc/$metadata)
dan komt alles beschikbaar:

<ComplexType Name="MachinesWithMinimumCountOfFailures_Result">
  <Property Name="MachineId"
            Type="Edm.Int32"
            Nullable="false" />
  <Property Name="MachineName"
            Type="Edm.String"
            Nullable="false"
            MaxLength="50" />
  <Property Name="SupervisorName"
            Type="Edm.String"
            Nullable="false"
            MaxLength="50" />
</ComplexType>
...
<FunctionImport Name="GetMachinesWithMinimumCountOfFailures"
                ReturnType="Collection(ODataModel.MachinesWithMinimumCountOfFailures_Result)"
                m:HttpMethod="GET">
   <Parameter Name="minimumCount"
              Type="Edm.Int32"
              Mode="In" />
</FunctionImport>

Mooi. Als we nu een selectie uitvoeren via de service operation…
http://localhost/WcfDataService1.svc/GetMachinesWithMinimumCountOfFailures?minimumCount=1

…dan krijgen we netjes alles informatie in het formaat van de complex
type terug.

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<GetMachinesWithMinimumCountOfFailures
  xmlns="htt p://schemas.microsoft.com/ado/2007/08/dataservices">
  <element p2:type="ODataModel.MachinesWithMinimumCountOfFailures_Result"
      xmlns:p2="htt p://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <MachineId p2:type="Edm.Int32">1</MachineId>
    <MachineName>MixerNew</MachineName>
    <SupervisorName>user</SupervisorName>
  </element>

  <element p2:type="ODataModel.MachinesWithMinimumCountOfFailures_Result"
      xmlns:p2="htt p://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <MachineId p2:type="Edm.Int32">2</MachineId>
    <MachineName>Machine that goes Ping</MachineName>
    <SupervisorName>user</SupervisorName>
  </element>

  <element p2:type="ODataModel.MachinesWithMinimumCountOfFailures_Result"
      xmlns:p2="htt p://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <MachineId p2:type="Edm.Int32">7</MachineId>
    <MachineName>Mr. Coffee</MachineName>
    <SupervisorName>Dark Helmet</SupervisorName>
  </element>

  <element p2:type="ODataModel.MachinesWithMinimumCountOfFailures_Result"
      xmlns:p2="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <MachineId p2:type="Edm.Int32">17</MachineId>
    <MachineName>Rage against the machine</MachineName>
    <SupervisorName>user</SupervisorName>
  </element>

  <element p2:type="ODataModel.MachinesWithMinimumCountOfFailures_Result"
      xmlns:p2="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
    <MachineId p2:type="Edm.Int32">18</MachineId>
    <MachineName>Mr. Radar</MachineName>
    <SupervisorName>Dark Helmet</SupervisorName>
  </element>

</GetMachinesWithMinimumCountOfFailures>

We hebben nu gezien dat het heel eenvoudig is om bestaande stored
procedures in een OData service op te nemen. Natuurlijk had deze logica ook
zonder de stored procedure dooe een client achterhaald kunnen worden maar voor meer complexere en specialistische selecties kan dit een uitkomst zijn.

Overigens wil ik nog even wijzen op een aardige bron van informatie
rond OData. Ik kan OData Primer aanbevelen vanwege de constant stroom van interessante links. Ik heb mijzelf op de RSS feed geabbonneerd en zojuist kwam (tijdens het schrijven van deze blog) een verwijziging voorbij naar een andere blog waarin bovenstaande techniek vergelijkbaar beschreven werd. Blijkbaar is het onsluiten van stored procedures een hot item 🙂

Liever lui dan moe: combineer MVC2 met Dynamic Data Web App

Bij mijn huidige MVC2 project zijn we in de testfase aanbeland. En we zitten
nu met een dilemma.

Hoewel MVC2 vooral voor CRUD applicaties toegepast zal worden, hoeft dit nog niet te betekenen dat ook alle vier de acties: create, read, update en delete geïmplementeerd gaan worden. De daadwerkelijke functionaliteit van de applicatie bepaalt namelijk wat er met de data gebeurt.

Lui

Dit maakt het functioneel testen dus niet eenvoudiger. Een applicatie welke alleen invoegen, wijzigen en tonen ondersteunt is heel erg cru (..) voor de tester. Hoe kan de functionaliteit getest worden rond het representeren van een lege selectie of tabel? Wat zal een gridview tonen: “geen data aanwezig”? Of zal er een foutmelding getoond worden? Dit is lastig te testen zonder dat de mogelijkheid bestaat om ook eens een record te verwijderen. Hetzelfde geldt voor het wijzigen van records waarvoor alleen invoeg-functionaliteit is geboden.

Daarom heb ik even een onderzoekje gedaan of ik een Dynamic Data Web
Application (DDWA) hiervoor kom toepassen. Ik heb hier in het verleden al eens over geblogd en dit leek mij wel weer een poging waard.

Nou, om kort te gaan, het kan en het is bijzonder eenvoudig.

Open dus eerst de VS2010 solution met daarin jouw MVC2 applicatie.
Voeg dan een DDWA project toe, in dit geval eentje voor het Entity
framework.

Voeg DDWA toe

In de meeste MVC2 applicaties is de Entity Framework context binnen
die applicatie gecreëerd. Voeg dus een referentie toe van het MVC2 project aan
het DDWA project.

Onthoudt nog even wat de naam is van de context. In mijn geval was
dat “DatabaseEntities”.

In de global.asax van moet vervolgens die regel uit het commentaar
gehaald worden en voorzien worden van de context naam:


DefaultModel.RegisterContext(typeof(DatabaseEntities), new
ContextConfiguration() { ScaffoldAllTables = true });

Standaard staat ScaffoldAllTables op false. Zet deze op true. Dit
forceert dat alle tabellen standaard beschikbaar zijn voor onderhoud via deze
DDWA applicatie.

Als laatste moet wel dezelfde database aangeroepen worden. Kopieer
dus de Entity Framework connectionstring uit de MVC2 applicatie en plaats deze
in de DDWA web.config:

<connectionStrings>

<add    

name=”DatabaseEntities”

connectionString=”metadata=res://*/Models.Model1.csdl|res://*/Models.Model1.ssdl|

res://*/Models.Model1.msl;provider=System.Data.SqlClient;

provider connection string=&quot;Integrated Security=SSPI;Persist
Security Info=False;

Initial Catalog=DATABASE.MDF;Data
Source=.\sqlexpress&quot;”

providerName=”System.Data.EntityClient”/>

</connectionStrings>

Let op, de database moet via een server gedeeld worden. Het
dynamisch koppelen van een Sql Server Express database (b.v. “Data
Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database1.mdf” ) werkt niet en zal bij de DDWA tot een vage foutmelding leiden.

En dat is alles. Makkelijk.

Nu kan via de DDWA applicatie snel onderhoud gepleegd worden op de (test) database. En nu kan ook snel inzichtelijk gemaakt worden hoe de ingevoerde informatie in de database opgeslagen wordt. Handig toch?

Eenvoudig unittesten voor Sql Server Integration Services

Sql Server Integration Services (SSIS) is het antwoord van Microsoft op de
vraag naar ETL. ETL staat voor Extract, Transform en Load. Eigenlijk wordt er
gewoon met data geschoven, van bron(-nen) naar doel(-en).

Mocht binnen een project veel met data import gewerkt worden dan kan SSIS vrijwel zeker een hele hoop problemen oplossen. SSIS geeft de mogelijkheid om bronnen en doelen te definiëren waarna een workflow ontworpen wordt om de data te extraheren, te valideren, te transformeren en weer weg te schrijven.

Input Output

Collega’s welke met BizTalk werken komt dit bekent voor maar daar is tevens
een workflow engine aanwezig, compleet met storage. Echter voor SSIS is de
workflow slechts een middel om data te transformeren. Door de grafische
representatie en eenvoudige maar krachtige scripting is SSIS ook een stuk
basisgereedschap voor administrators.

Er is een leerkurve om met SSIS aan de gang te gaan maar hier en hier staat voldoende om er eens mee te beginnen.

Een stuk SSIS logica (package) kan zowel direct in Sql Server bewaard worden,
maar ook op schijf opgeslagen worden.  Zo’n package krijgt de extensie DTSX.
Hoewel opslag op schijf de package kwetsbaar maakt voor systeem-afhankelijke
paden, heeft het wel degelijk voordelen. De package wordt zo makkelijk
transportabel in een OTAP omgeving met verschillende SQL servers en dit maakt de package ook beschikbaar voor unittesten.

Want soms lijkt iets heel lastig maar is het in wezen juist er simpel. Binnen
ons team zijn wij met de bouw van Business Intelligence Software (BIS) begonnen waarbij SSIS de basis is voor de import van data en bestanden uit csv bestanden, mappen en webservices. En hierbij kwam al snel de vraag hoe we konden bewijzen dat onze import met verschillende scenario’s kon omgaan.

Uiteindelijk bleek de oplossing heel eenvoudig te zijn. Voor SSIS is ook een
.Net API beschikbaar. Het is hiermee zelfs mogelijk om SSIS packages volledig
uit te coderen maar daar waren wij niet in geïnteresseerd. Wel is het mogelijk
om zo via code een package te laden, de variabelen te vullen en uit te voeren.
Vervolgens is te controleren of de package correct uitgevoerd is. Uiteindelijk
kan gecontroleerd worden of de package correct gereageerd heeft op de
gecontroleerde input.

Hoewel dit meer een integratie test is omdat het mocken van services
en databases hierbij wat lastig is, scheelt deze aanpak veel tijd voor het
ontwikkelteam. Anders moet tijdens het coderen steeds handmatig een scenario
afgewerkt worden en bij iedere wijziging mogen die weer opnieuw gecontroleerd
worden.

Maak dus naast het SSIS project een unittest project aan en voeg
onderstaande references toe.

SSIS API Assemblies

Maak vervolgens een unittest aan welke de package in het geheugen laadt.

In onderstaande voorbeeld wordt eerst een csv uitgelezen. Vervolgens wordt
een sommatie weggeschreven in een ander bestand. Bus beide bestandsnamen worden opgegeven bij de package.

using System;using System.Text;using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.SqlServer.Dts.Runtime;
using Microsoft.SqlServer.Dts.Pipeline;
using SSISRumtime = Microsoft.SqlServer.Dts.Runtime.Wrapper;
using System.IO;
namespace TestProject
{
  [TestClass]
  public class UnitTest1
  {

  [TestMethod]
  public void TestMethod1()
  {
    Application application = new Application();
    Package package = null;
    try
    {
      string inputFile =
        @"C:\TMP\Integration Services
        Project\TestProject\InputFiles\Test1.txt";
      string outputFile =
        @"C:\TMP\Integration Services
        Project\TestProject\OutputFiles\Test1.txt";
      package = application.LoadPackage(
        @"C:\TMP\Integration Services Project\Integration Services
        Project\bin\Package.dtsx", null);
      package.Connections["ImportTest"].ConnectionString =
        inputFile;
      package.Connections["ImportResult"].ConnectionString =
        outputFile;
      Microsoft.SqlServer.Dts.Runtime.DTSExecResult results =
        package.Execute();

      Assert.AreEqual(Microsoft.SqlServer.Dts.Runtime.DTSExecResult.Success,
        results, "Execution of package failed");
      Assert.IsTrue(File.Exists(outputFile));
      string outputTextExpected =
        "<html><body>1000</body></html>";
      string outputTextActual = "";
      using (StreamReader streamReader =
                           File.OpenText(outputFile))
      {
        outputTextActual = streamReader.ReadToEnd();
      }
      Assert.AreEqual(outputTextExpected, outputTextActual);
    }
    finally
    {
      package.Dispose();
      package = null;
    }
  }
}
}

Omdat we weten wat we als invoer meegeven, weten we ook wat we mogen
verwachten.

We roepen dus Execute op de package aan en controleren of de succesvol is
uitgevoerd. Maar we moeten wel het resultaat ook controleren. Daarom wordt
uiteindelijk het doelbestand uitgelezen en op inhoud vergeleken.

SSIS Unittest passed

Uiteindelijk levert deze test een groen vinkje op. Yes! (en vieren met een
kopje koffie, want: “Vier je successen”).

Dit geeft een aardige start.

Mocht dit  uiteindelijk als unittest mechanisme te licht bevonden worden, dan
zijn er ook zwaardere middelen aanwezig.