Week 12 - reflectie

reflectie

Maandag - integratie tests

Bij het maken van de AngularJS controllers is gebleken dat een aantal Web API controllers toch niet doen wat ze moeten doen, ondanks dat de unit-tests zijn geslaagd. Blijkbaar heb ik de database niet gemocked op een manier dat hetzelfde gedrag geeft als in de werkelijke applicatie.

Op basis hiervan heb ik tóch besloten integraties tests te maken. Daarbij wordt daadwerkelijk de REST API van de draaiende applicatie aangesproken. Ik gebruik daarvoor de HttpClient klasse van het .NET framework in combinatie van de unit-test functionaliteit van Visual Studio. Het idee is dat mijn Rest client alle resources afloopt en daarop de verschillende HTTP methoden uitvoert. Als deze allen lukken, mag ik er vanuit gaan dat de API naar behoren werkt.

Nu heb ik een kleine 30 resources die alleen hun eigen klassen teruggeven, maar ik wil niet voor al die resources verschillende code schrijven. Ik maak daarom gebruik van generics. Hierdoor wordt de klasse pas bij het instantiëren type specifiek gemaakt en kan ik dus één implementatie gebruiken voor meerdere klassen.

De generieke RestClient klasse ziet er als volgt uit:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http;
using System.Net.Http.Headers;
namespace hueProductDatabase.IntegrationTests {
  public class RestClient {
    private HttpClient httpClient;
    public RestClient(String baseAddress, int timeout) {
      httpClient = new HttpClient();
      httpClient.BaseAddress = new Uri(baseAddress);
      httpClient.DefaultRequestHeaders.Accept.Clear();
      httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
      httpClient.Timeout = new TimeSpan(0, 0, timeout);
    }
    public async Task < List < T >> GetAll < T > (String resource) {
      HttpResponseMessage response = await httpClient.GetAsync(resource).ConfigureAwait(false);
      if (response.IsSuccessStatusCode) {
        List < T > result = await response.Content.ReadAsAsync < List < T >> ().ConfigureAwait(false);
        return result;
      }
      return
    default (List < T > );
    }
    public async Task < T > GetItem < T > (String resource, int id) {
      HttpResponseMessage response = await httpClient.GetAsync(resource + "/" + id.ToString());
      if (response.IsSuccessStatusCode) {
        T result = await response.Content.ReadAsAsync < T > ();
        return result;
      }
      return
    default (T);
    }
    public async Task < T > PostItem < T > (String resource, T item) {
      HttpResponseMessage response = await httpClient.PostAsJsonAsync(resource, item);
      if (response.IsSuccessStatusCode) {
        T result = await response.Content.ReadAsAsync < T > ();
        return result;
      }
      return
    default (T);
    }
    public async Task < Boolean > PutItem < T > (String resource, int id, T item) {
      HttpResponseMessage response = await httpClient.PutAsJsonAsync(resource + "/" + id.ToString(), item);
      if (response.IsSuccessStatusCode) {
        return true;
      }
      return false;
    }
    public async Task < bool > DeleteItem(string resource, int id) {
      HttpResponseMessage response = await httpClient.DeleteAsync(resource + "/" + id.ToString());
      if (response.IsSuccessStatusCode) {
        return true;
      }
      return false;
    }
  }
}

En zo is ook de test opgezet voor de diverse klassen afgeleid van de Release klasse:

private async Task < TestResults < T >> doSimpleReleasesTest < T > () where T: Release {
  TestResults < T > results = new TestResults < T > ();
  String resource = typeof(T).Name + "s";
  results.ReleaseItemList = await restClient.GetAll < T > (resource);
  Assert.IsNotNull(results.ReleaseItemList, "GetAll should result in a (empty) list, unless the resource cannot be found.");
  Assert.IsTrue(results.ReleaseItemList.Count > 0, "GetAll must result in 1 or more items in the list.");
  if (results.ReleaseItemList.Count > 1) {
    T releaseItemFromList = results.ReleaseItemList[0];
    results.ReleaseItem = await restClient.GetItem < T > (resource, releaseItemFromList.Id);
    Assert.IsNotNull(results.ReleaseItem, "GetItem must result in 1 item from the previous list.");
    T releaseItem = CloneViaJson.Clone < T > (results.ReleaseItem);
    releaseItem.Id = 0;
    releaseItem.ReleaseName = "NewPost";
    results.ReleaseItemAfterPost = await restClient.PostItem < T > (resource, releaseItem);
    Assert.IsNotNull(results.ReleaseItemAfterPost, "PostItem must result in posted item.");
    Assert.IsTrue(results.ReleaseItemAfterPost.Id > 0, "PostItem must result in posted item with Id > 0.");
    Assert.AreEqual(releaseItem.ReleaseName, results.ReleaseItemAfterPost.ReleaseName, "ReleaseName in posted item must be NewPost.");
    results.ReleaseItemAfterPost = await restClient.GetItem < T > (resource, results.ReleaseItemAfterPost.Id);
    Assert.IsNotNull(results.ReleaseItemAfterPost);
    releaseItem = CloneViaJson.Clone < T > (results.ReleaseItemAfterPost);
    releaseItem.ReleaseName = "NewPut";
    results.PutResult = await restClient.PutItem < T > (resource, releaseItem.Id, releaseItem);
    Assert.IsTrue(results.PutResult, "PutItem must report successful.");
    if (results.PutResult) {
      results.ReleaseItemAfterPut = await restClient.GetItem < T > (resource, releaseItem.Id);
      Assert.IsNotNull(results.ReleaseItemAfterPut);
      Assert.AreEqual(releaseItem.ReleaseName, results.ReleaseItemAfterPut.ReleaseName, "ReleaseName in posted item must be NewPut.");
      results.DeleteResult = await restClient.DeleteItem(resource, results.ReleaseItemAfterPut.Id);
      Assert.IsTrue(results.DeleteResult, "DeleteItem must report successful.");
      Console.WriteLine(resource + " done!");
    }
  }
  return results;
}

De test definities zijn daarmee beperkt tot onderstaande voor elke REST resource:

[TestMethod]
public async Task TestIntegration_SystemReleases() {
  var results =  await doSimpleReleasesTest < SystemRelease > ();
}

private class TestResults < T > where T: Release {
  /// <summary>
  /// Result of GetAll call
  /// </summary>
  public List < T > ReleaseItemList {
    get;
    set;
  }
  /// <summary>
  /// Result of GetItem call
  /// </summary>
  public T ReleaseItem {
    get;
    set;
  }
  /// <summary>
  /// Result of PostItem call
  /// </summary>
  public T ReleaseItemFromPost {
    get;
    set;
  }
  /// <summary>
  /// Result of GetItem of previous PostItem call
  /// </summary>
  public T ReleaseItemAfterPost {
    get;
    set;
  }
  /// <summary>
  /// Result of PutItem call
  /// </summary>
  public bool PutResult {
    get;
    set;
  }
  /// <summary>
  /// Result of GetItem of previous PutItem call
  /// </summary>
  public T ReleaseItemAfterPut {
    get;
    set;
  }
  /// <summary>
  /// Result of DeleteItem call
  /// </summary>
  public bool DeleteResult {
    get;
    set;
  }
}

Door middel van de TestResults klasse zijn eventueel nog extra controles uit te voeren op de resultaten van de standaard tests.

Dinsdag - Viewmodel klasse of andere query?

Uit de integratie test van gister is gebleken dat de AppSoftwareReleaseController niet werkt, ondanks de geslaagde unit-test. Nog 16 controllers falen op vergelijkbare wijze. De integratie test heeft hiermee al direct zijn dienst bewezen!

De AppSoftwareRelease klasse heeft een ClipApiRelease property. Dit is een object dat uit een andere database tabel moet komen middels een join. De query daarvoor ziet er als volgt uit:

IQueryable < AppSoftwareRelease > queryResult;
queryResult = (from asr in db.AppSoftwareReleases join car in db.ClipApiReleases on asr.Id equals car.Id into car_ from car in car_.DefaultIfEmpty() select new AppSoftwareRelease {
  Id = asr.Id,
  ReleaseDate = asr.ReleaseDate,
  ReleaseDescription = asr.ReleaseDescription,
  ReleaseName = asr.ReleaseName,
  ReleaseNotesUrl = asr.ReleaseNotesUrl,
  ReleaseStatus = asr.ReleaseStatus,
  SoftwareReleaseType = asr.SoftwareReleaseType,
  SupportedClipApiReleaseId = asr.SupportedClipApiReleaseId,
  Version = asr.Version,
  SupportedClipApiRelease = car
});

Helaas genereert deze oplossing een exception:

"The entity or complex type 'hueProductDatabase.Models.AppSoftwareRelease' cannot be constructed in a LINQ to Entities query."

De AppSoftwareRelease is ook al een entiteit type in de database en blijkbaar vind LINQ to Entities dubbel gebruik hiervan niet leuk….

Oplossing is het maken van een view model klasse. Een klasse alleen maar bedoelt om een view mee te genereren. De query blijft gelijk, maar in plaats van een AppSoftwareRelease object wordt er een AppSoftwareReleaseView object gegenereerd:

IQueryable < AppSoftwareReleaseView > queryResult;
queryResult = (from asr in db.AppSoftwareReleases join car in db.ClipApiReleases on asr.Id equals car.Id into car_ from car in car_.DefaultIfEmpty() select new AppSoftwareReleaseView {
  Id = asr.Id,
  ReleaseDate = asr.ReleaseDate,
  ReleaseDescription = asr.ReleaseDescription,
  ReleaseName = asr.ReleaseName,
  ReleaseNotesUrl = asr.ReleaseNotesUrl,
  ReleaseStatus = asr.ReleaseStatus,
  SoftwareReleaseType = asr.SoftwareReleaseType,
  SupportedClipApiReleaseId = asr.SupportedClipApiReleaseId,
  Version = asr.Version,
  SupportedClipApiRelease = car
});

Dit werkt. Echter, ik moet een AppSoftwareReleaseView klasse definiëren dat feitelijk een één op één kopie is van de AppSoftwareRelease klasse. Deze kopieerslag is extra werk en resulteert mogelijk in fouten.

Een andere mogelijkheid is deze:

public IEnumerable < AppSoftwareRelease > GetAppSoftwareReleases() {
  IEnumerable < AppSoftwareRelease > queryResult = (from ca in db.AppSoftwareReleases select new {
    AppSoftwareRelease = ca,
    ca.SupportedClipApiRelease
  }).AsEnumerable().Select(ca =>ca.AppSoftwareRelease);
  return queryResult;
}

Bij deze oplossing vertelt het eerste select deel LINQ to SQL dat er een join nodig is voor property SupportedClipApiRelease. Met methode .AsEnumerable() wordt het LINQ to SQL resultaat van het eerste deel van de query omgezet naar een LINQ to Objects query, waarbij middels de laatste select het AppSoftwareRelease object waar we naar zoeken wordt teruggeven. Inclusief de gevulde SupportedClipApiRelease property!

Meer details over deze laatste methode kun je hier vinden.

Een kort vergelijk tussen beide methodes door het uitvoeren van een integratie test toont ook nog aan dat er nauwelijks verschil in performance is (beide methoden kosten zo’n 450ms) en de keuze is daarmee helder en valt op de laatste.

maandag 27 april 2015