Testen von Datenbanken - 3 Testfälle in separater Testdatenbank 26.03.2015

Alexander Kabisch
Alexander Kabisch, Principal eXpert

Bei diesem Vorgehen werden die Testfälle gegen eine eigene Testdatenbank ausgeführt, die von der Entwicklungsdatenbank losgelöst ist. Diese Umgebung wird dann mit Testdaten befüllt und im Test verwendet.

Dieses Vorgehen trifft man ebenfalls sehr häufig, dabei wird eine zweite zur Entwicklung strukturgleiche Umgebung aufgebaut. Sie muss immer auf dem Stand der Entwicklung gebracht werden! Die Daten werden genau auf die Testfälle zugeschnitten und müssen erst aufgebaut werden. Der Testcode entspricht dabei sogar dem vorherigen Vorgehen.

Testablauf am Beispiel der Referenzanwendung

  1. Verbindung zur Testdatenbank wird geöffnet.
  2. Es wird davon ausgegangen dass die Daten in der Tabelle [dbo].[Source] zum 1.1.2014 vorliegen.
  3. Die Tabelle [dbo].[Target_Sum] wird gelöscht.
    • [dbo].[Delete_Target_Sum_ByCalcDate]
  4. Die Tabelle [dbo].[Target_Sum] wird befüllt.
    • [dbo].[Insert_Target_ByCalcDate]
  5. Es wird geprüft ob die Daten wie erwartet in der Tabelle [dbo].[Target_Sum] vorliegen.
    • Die Daten werden vom Test in eine Datei exportiert.
    • Die Datei wird mit einer erwarteten Datei verglichen.
[TestMethod]
public void DALTest_SeperateDB()
{
    DBManagerTarget mgr = new DBManagerTarget();
    DateTime calcDate = new DateTime(2014, 01, 01);
    mgr.DeleteTargetSum(calcDate);
    mgr.FillTarget(calcDate);
 
    DBManager testDatenmanger = new DBManager("DB", false);
 
    string filename = "DALTest_SeperateDB_UnitTest1.txt";
    testDatenmanger.ReadToFile(
        DIRTARGET + filename,
        "SELECT * FROM dbo.Target_Sum WHERE CalcDate='2014-01-01';");
 
    CompareHelper.AssertAreEual(DIRSOURCE + filename, DIRTARGET + filename);
}
<connectionStrings>
  <clear/>
  <add name="DB" connectionString="... Initial Catalog=DBTestingSeperate; ..."/>
</connectionStrings>

Ergebnis: Erfolgreich

SELECT * FROM dbo.Target_Sum WHERE CalcDate='2014-01-01';
 
CalcDate(datetime),ID(int),Value(float)
 
01.01.2014 00:00:00,1,10
01.01.2014 00:00:00,2,20
01.01.2014 00:00:00,3,30
01.01.2014 00:00:00,4,40

Die Tabelle [dbo].[Target_Code] ist in der Datenbank noch leer oder genau mit den passenden IDs gefüllt. Die Testkonstellation muss passen, ansonsten ist der Testfall nicht erfolgreich.

Vorteile

  • Aufwand beim Erstellen der Tests hält sich in Grenzen
  • Daten sind exakt auf die Test zugeschnitten
    • Entwicklungsdaten werden nicht geändert
    • Referenzierte Daten könnten schon vorliegen

Nachteile

  • Weitere Umgebung die aktuell gehalten werden muss
  • Möglicher hoher Aufwand bei neuen Testszenarien
    • Sich gegenseitig beeinflussende Tests
    • Testdaten müssen erzeugt und integriert werden

Dieses Vorgehen ist schon mal ein ganzes Stück besser als das vorherige, bei dem die Tests auf der Entwicklungsdatenbank ausgeführt werden. Leider könnten sich hierbei Testfälle gegenseitig beeinflussen oder die Erstellung neuer Testfälle schwierig machen.

zur Übersicht

Share |

LINQ Coding Guidelines #8–Provider nicht mischen 19.03.2015

Alexander Jung
Alexander Jung, Chief eXpert

Der LINQ-Provider hat nicht unerheblichen Einfluss auf die Ausführung der LINQ-Ausdrücke. Sich dessen bewusst zu sein ist wichtig, diesen Umstand für alle Leser deutlich zu machen nicht minder.

Empfehlung: LINQ-Abfragen die in relevanten Anteilen unterschiedliche Provider betreffen sollten entsprechend getrennt werden.

 

Fangen wir wieder mit einem Beispiel an:

   1: public static IEnumerable<object> GetSystemsForApplication(this SystemRepository repository, string applicationId, bool includeWithdrawn)
   2: {
   3:     return (from item in repository.ServersThisApplicationDependsOn
   4:             where (item.CONSUMING_APPLICATION == applicationId) && (includeWithdrawn || (item.DEPENDENCY_STATUS != WithDrawnIdentifier))
   5:             orderby item.SERVER_NAME
   6:             select item).AsEnumerable().Select(item => new
   7:             {
   8:                 item.CONSUMING_APPLICATION,
   9:                 item.SERVER_NAME,
  10:                 item.DEPENDENCY_STATUS,
  11:                 item.SERVER_STATUS,
  12:                 item.SERVER_OS,
  13:                 item.SERVER_LOCATION,
  14:                 item.SERVER_FQDN,
  15:                 LATEST_TARGET_HARDWARE_REMOVAL_DATE = GetFormatedNullableDateTime(item.LATEST_TARGET_HARDWARE_REMOVAL_DATE)
  16:             });
  17: }

Gesehen?

Der expliziten Aufruf von AsEnumerable() führt dazu, dass der erste Teil der Abfrage gegen das Entity Framework ausgeführt wird, der nachfolgende Teil hingegen gegen LINQ-to-Objects. Im vorliegenden Fall bleibt das weitgehend ohne Konsequenzen, ich habe aber auch schon Beispiele gesehen, wo das nicht der Fall ist. Zumindest dürfte nicht jedem Entwickler sofort ersichtlich sein, was hier passiert – die Formatierung tut ein Übriges um das untergehen zu lassen.

 

Ein weiteres Beispiel ist die Suchfunktion die ich in einem der letzten Beiträge schon gezeigt habe:

   1: public Collection<CashTransactionBE> SearchTransactions(CashTransactionSearchCriteriaBE searchCriteria, long accountId)
   2: {
   3:     using (EvolutionDBEntities entities = new EvolutionDBEntities())
   4:     {                
   5:         var condition = LinqHelper.True<Transaction>();
   6:         var conditionBalance = condition.And(t => (((searchCriteria.MinimumAmount == null) || (t.Amount >= searchCriteria.MinimumAmount)) &&
   7:                                                   ((searchCriteria.MaximumAmount == null) || (t.Amount <= searchCriteria.MaximumAmount))));
   8:         var conditionDate = conditionBalance.And(t => (((searchCriteria.StartDate == null) || (t.Date > searchCriteria.StartDate.Value.AddDays(-1))) &&
   9:                                                       ((searchCriteria.EndDate == null) || (t.Date < searchCriteria.EndDate.Value.AddDays(1)))));
  10:         string transactionType = GetTransactionType(searchCriteria.CashTransactionType);
  11:         var conditionTransactionType = conditionDate.And(t => ((searchCriteria.CashTransactionType == TransactionType.All) || (transactionType == t.Type)));
  12:  
  13:         IEnumerable<Transaction> result = entities.Transaction
  14:             .Where(t => t.AccountID == accountId)
  15:             .Where(conditionTransactionType.Compile());
  16:  
  17:         return result.Select(t => t.ToBE()).ToCollection();
  18:     }
  19: }

Na?

Dieser Fall ist schon hinterhältiger: Die erste Filterung nach der AccountID findet noch gegen das Entity Framework, d.h. effizient in der Datenbank, statt. Der zweite Filter (mit condition.Compile()) geht jedoch nicht gegen Queryable, sondern gegen Enumerable, weil Compile() keine Expression, sondern einen Delegate liefert. (Kein neues Thema.) Den Filter nicht in der Datenbank sondern im Speicher anzuwenden war hier sicher nicht im Sinne des Erfinders. Oder doch? Ist die erwartete Datenmenge so gering, dass das hier keinen Unterschied macht? Wir wissen es nicht, genauso wenig wie der Entwickler, der den Code irgendwann übernehmen und pflegen muss – und der jetzt vor einem Wartungsproblem steht.

 

In beiden Beispielen findet bei der Abfrage ein Wechsel des Providers statt. Und in beiden Fällen ist der Leser mit der Frage alleine gelassen, ob sich der Autor der Problematik bewusst war. Schlimmer: bei einer späteren Anpassung des Codes könnte ein Entwickler zum Zuge kommen, der sich der Problematik nicht bewusst ist und der das Kind nachträglich durch Änderungen in den Brunnen schubst.

Ergo sollten solche Wechsel des Providers im Code deutlich nachvollziehbar dokumentiert werden, etwa:

   1: public static IEnumerable<object> GetSystemsForApplication(this SystemRepository repository, string applicationId, bool includeWithdrawn)
   2: {
   3:     var result= (from item in repository.ServersThisApplicationDependsOn
   4:             where (item.CONSUMING_APPLICATION == applicationId) && (includeWithdrawn || (item.DEPENDENCY_STATUS != WithDrawnIdentifier))
   5:             orderby item.SERVER_NAME
   6:             select item);
   7:     // wegen Hilfsfunktion beim Mapping im Speicher durchführen:
   8:     return result.AsEnumerable().Select(item => new
   9:             {
  10:                 item.CONSUMING_APPLICATION,
  11:                 item.SERVER_NAME,
  12:                 item.DEPENDENCY_STATUS,
  13:                 item.SERVER_STATUS,
  14:                 item.SERVER_OS,
  15:                 item.SERVER_LOCATION,
  16:                 item.SERVER_FQDN,
  17:                 LATEST_TARGET_HARDWARE_REMOVAL_DATE = GetFormatedNullableDateTime(item.LATEST_TARGET_HARDWARE_REMOVAL_DATE)
  18:             });
  19: }

Und:

   1: IEnumerable<Transaction> result = entities.Transaction.Where(t => t.AccountID == accountId);
   2: // Abfrage im Speicher! (Aufgrund der geringen Datenmenge vertretbar!)
   3: result = result.Where(conditionTransactionType.Compile());

Jetzt sind die Unklarheiten beseitigt.

Leider ist an den Aufrufen selbst nicht erkennbar, ob sie gegen Queryable oder Enumerable gehen. Warnsignale sind notwendige Aufrufe von AsQueryable() oder AsEnumerable(). Der implizite Wechsel durch den Typ des Delegates ist hinterhältiger, aber glücklicherweise auch seltener.

Share |

Export to Excel using … 17.03.2015

Matthias Straßer
Matthias Straßer
Chief eXpert

Bei Anwendungen, die große Datenmengen verarbeiten, wird sehr häufig die Anforderung gestellt, die Daten in Excel bereit zu stellen, da die Möglichkeiten der Analyse oder der Weiterverarbeitung hier schneller und flexibler von statten gehen kann. Die Funktionalität des Excels in der Anwendung bereitzustellen ist auch nicht sinnvoll, obwohl mit Hilfe von Controls von Anbietern wie Telerik bereits sehr viel machbar ist. Nun stellt sich die Frage, welche Optionen habe ich, um Daten nach Excel zu exportieren.

Die nachfolgenden Beschreibungen und Beobachtungen basieren auf einer WPF Anwendung mit Office 2010. Als Optionen für den Export wurden betrachtet

  1. Excel Automation
  2. ClosedXML (Codeplex)
  3. EPPlus (Codeplex)
  4. Telerik RadGridView Control

Die kleine WPF-Anwendung hat lediglich ein DataGrid, in denen die zu exportierenden Daten angezeigt werden, eine Methode um eine bestimmte Anzahl an Daten zu laden und vier Methoden um die Daten nach Excel zu exportieren. Die Daten bestehen aus einer Liste von Objekten mit lediglich 4 Eigenschaften, die zufällig generiert werden. In allen vier Fällen wurden unterschiedliche Anzahlen an Datensätze exportiert und dabei die Laufzeit und der temporäre maximale zusätzliche Verbrauch an Arbeitsspeicher (Working Set (Memory)) beobachtet. Die Beobachtungen betrachten nur den ersten Aufruf des Exports.

image

Beim Export ist keine besondere Formatierung vorgenommen worden. Dies ist aber bei allen 4 Lösungen machbar. Ebenso wurden die Methoden gewählt, die den geringsten Aufwand hatten.

1. Option: Excel Automation

Microsoft stellt für seine Office-2010-Programme Primary Interop Assemblies (PIA) bereit, welche über ein Restributable Paket separat installiert werden können. Im Projekt muss nun eine Reference auf das Assembly „Microsoft.Office.Interop.Excel.dll“ aus dem Verzeichnis C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Interop.Excel\ 14.0.0.0__71e9bce111e9429c\ gesetzt werden.

   1: public void SaveWithPIA(IEnumerable<BusinessItem> businessItems)
   2: {
   3:   var resultFile = @"c:\temp\ExportPIA.xlsx";
   4:   if (File.Exists(resultFile))
   5:   {
   6:     File.Delete(resultFile);
   7:   }
   8:  
   9:   var application = new Microsoft.Office.Interop.Excel.Application();
  10:   application.Visible = false;
  11:   application.UserControl = false;
  12:   try
  13:   {
  14:     var workbook = application.Workbooks.Add();
  15:     var worksheet = workbook.ActiveSheet as Microsoft.Office.Interop.Excel.Worksheet;
  16:  
  17:     this.ExportItems(
  18:       worksheet, 
  19:       new List<string> { "ID", "Name", "UnitPrice", "Date" },
  20:       businessItems.ToList(), 
  21:       new List<Func<BusinessItem, object>>
  22:       {
  23:         p => p.ID, 
  24:         p => p.Name, 
  25:         p => p.UnitPrice,
  26:         p => p.Date,
  27:       });
  28:  
  29:     workbook.SaveAs(resultFile);
  30:   }
  31:   finally
  32:   {
  33:     application.Quit();
  34:     application = null;
  35:   }
  36: }
  37:  
  38: private void ExportItems<T>(
  39:   Microsoft.Office.Interop.Excel.Worksheet worksheet, 
  40:   List<string> headers,
  41:   List<T> items, 
  42:   List<Func<T, object>> valueGetters)
  43:    where T : class
  44: {
  45:   var headerArray = new object[1, headers.Count];
  46:  
  47:   for (int i = 0; i < headers.Count; i++)
  48:   {
  49:     headerArray[0, i] = headers[i];
  50:   }
  51:  
  52:   var leftUpperCell = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[1, 1];
  53:   var rightLowerCell = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[1, headers.Count];
  54:   var headerRow = worksheet.Range[leftUpperCell, rightLowerCell];
  55:   headerRow.Value = headerArray;
  56:   headerRow.Font.Bold = true;
  57:  
  58:   var dataArray = new object[items.Count, valueGetters.Count];
  59:  
  60:   for (var rowIndex = 0; rowIndex < items.Count; rowIndex++)
  61:   {
  62:     T item = items[rowIndex];
  63:     var columnIndex = 0;
  64:     foreach (var valueGetter in valueGetters)
  65:     {
  66:       dataArray[rowIndex, columnIndex] = valueGetter(item);
  67:       columnIndex++;
  68:     }
  69:   }
  70:  
  71:   leftUpperCell = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[2, 1];
  72:   rightLowerCell = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[1 + items.Count, valueGetters.Count];
  73:   var dataRange = worksheet.Range[leftUpperCell, rightLowerCell];
  74:   dataRange.Value = dataArray;
  75: }

Vorteile

Bei größeren Datenmengen (100.000 Datensätze und mehr) ist diese Art des Exports mit annehmbarer Performance und auch Memory-Verbrauch verbunden. Die erzeugten Excel-Files sind von der Größe her mit einem „normalen“ Excel vergleichbar, was ja auch nicht verwunderlich ist, denn es wird ja Excel verwendet. Im Zuge eines MVVM-Pattern, kann der Export in einem ViewModel oder einer Service/Repository-Komponente implementiert werden.

Nachteile

Es ist auf dem System, wo der Export-Vorgang ausgeführt wird, die Installation von Excel notwendig. Dies kann bei einem Server gestütztem Vorgang eventuell Probleme bereiten, weil eine Office-Installation auf einem Server vom Betrieb nicht gerne gesehen wird. Ebenso muss die Liste der Objekte im Arbeitsspeicher „doppelt“ vorgehalten werden, da ein Objekt-Array erzeugt werden muss, um die Daten sinnvoll in die Excel-Zellen zu kopieren. Das Setzen des Wertes über eine einzelne Zelle verschlechtert die Performance erheblich.

Datensätze 100 1.000 10.000 100.000 1.000.000
Zuwachs
Arbeitsspeicher + Excel (K)
5.000 +
100.000
5.000 +
100.000
7.000 +
100.000
40.000 +
100.000
430.000 +
410.000
Zeit (s) 3-4 3-4 3-4 4-6 20-22
Dateigröße (KB) 14 37 261 2.545 25.420

Für Excel 2013 sind hier weitere Informationen zu finden.

2. Option: ClosedXML

ClosedXML ist eine unter MIT License verfügbare Bibliothek auf Codeplex. Hier heißt es

„ClosedXML makes it easier for developers to create Excel 2007/2010 files. It provides a nice object oriented way to manipulate the files (similar to VBA) without dealing with the hassles of XML Documents. It can be used by any .NET language like C# and Visual Basic (VB).“

Sie basiert auf dem OpenXml Format und benötigt die Library DocumentFormat.OpenXml.dll (Download). Nach der Referenzierung der beiden DLL’s „ClosedXML.dll“und „DocumentFormat.OpenXml.dll“ kann eine Liste von Objekten wie im Listing zu sehen gespeichert werden.

   1: public void SaveWithOpenXml(IEnumerable<BusinessItem> businessItems)
   2: {
   3:   var strResultFile = @"c:\temp\ExportClosedXml.xlsx";
   4:   if (File.Exists(strResultFile))
   5:   {
   6:     File.Delete(strResultFile);
   7:   }
   8:  
   9:   using (var workbook = new XLWorkbook(XLEventTracking.Disabled))
  10:   {
  11:     using (var worksheet = workbook.Worksheets.Add("ExportData"))
  12:     {
  13:       worksheet.Cell(1, 1).InsertTable(businessItems);
  14:     }
  15:  
  16:     workbook.SaveAs(strResultFile);
  17:   }
  18: }

Vorteile

Es ist keine Excel-Installation auf dem ausführenden System notwendig. Die erzeugten Excel-Files sind von der Größe her mit einem „normalen“ Excel vergleichbar. Im Zuge eines MVVM-Pattern, kann der Export in einem ViewModel oder einer Service/Repository-Komponente implementiert werden. Ebenso ist eine akzeptable Performance bei kleineren Datenmengen zu beobachten.

Nachteile

Der zusätzliche Bedarf an Arbeitsspeicher bei größeren Datenmengen ist enorm.

Datensätze 100 1.000 10.000 100.000 1.000.000
Zuwachs
Arbeitsspeicher (K)
12.000 24.000 50.000 520.000 4.700.000
Zeit (s) 1-2 1-2 2-3 8-10 90-110
Dateigröße (KB) 11 30 218 2.114 21.091

3. Option: EPPlus

EPPlus ist eine unter GNU Library General Public License verfügbare Bibliothek auf Codeplex. Hier heißt es

„EPPlus is a .net library that reads and writes Excel 2007/2010 files using the Open Office Xml format (xlsx).“

Nach der Referenzierung der DLL „EPPlus.dll“ kann eine Liste von Objekten wie im Listing zu sehen gespeichert werden.

   1: public void SaveWithEpplus(IEnumerable<BusinessItem> businessItems)
   2: {
   3:   var strResultFile = @"c:\temp\ExportEpplus.xlsx";
   4:   if (File.Exists(strResultFile))
   5:   {
   6:     File.Delete(strResultFile);
   7:   }
   8:  
   9:   var fileInfo = new FileInfo(strResultFile);
  10:  
  11:   using (var excelPackage = new ExcelPackage(fileInfo))
  12:   {
  13:     var worksheet = excelPackage.Workbook.Worksheets.Add("ExportData");
  14:     
  15:     worksheet.Cells.LoadFromCollection(businessItems, true);
  16:  
  17:     excelPackage.Save();
  18:   }
  19: }

Vorteile

Es ist keine Excel-Installation auf dem ausführenden System notwendig. Die erzeugten Excel-Files liegen von der Größe her leicht unter dem „normalen“ Excel. Im Zuge eines MVVM-Pattern, kann der Export in einem ViewModel oder einer Service/Repository-Komponente implementiert werden. Sehr performant bei kleineren und mittleren Datenmengen, sehr sparsam im Arbeitsspeicherverbrauch.

Nachteile

Die aktuellste Version (4.0.2) hat beim Speichern eine Exception geworfen, deswegen wurde im Test die Version 3.1 verwendet. Das Datumsfeld wurde im Excel nicht als Datum formatiert, sondern es steht dort der numerische Wert.

Datensätze 100 1.000 10.000 100.000 1.000.000
Zuwachs
Arbeitsspeicher (K)
4.000 5.000 10.000 105.000 780.000
Zeit (s) <1 <1 <1 6-7 60-70
Dateigröße (KB) 5 23 197 1.986 19.819

4. Option: Telerik RadGridView

Telerik ist einer der professionellen Anbieter von Controls um Anwendungen mit mehr User-Expierence auszustatten. Das RadGridView bietet eine Export Funktion an, die es auch ermöglicht Excel-kompatible Dateien zu erzeugen.

Eine Code-Behind Methode könnte dann so aussehen

   1: private void ExportWithTelerik(object sender, RoutedEventArgs e)
   2: {
   3:   try
   4:   {
   5:     using (var stream = File.OpenWrite(@"c:\temp\ExportTelerik.xls"))
   6:     {
   7:       var exportOptions = new GridViewExportOptions();
   8:       exportOptions.Format = ExportFormat.ExcelML;
   9:       exportOptions.ShowColumnFooters = false;
  10:       exportOptions.ShowColumnHeaders = true;
  11:       exportOptions.ShowGroupFooters = false;
  12:       exportOptions.Encoding = Encoding.UTF8;
  13:  
  14:       ExportGridView.Export(stream, exportOptions);
  15:     }
  16:   }
  17:   catch (Exception ex)
  18:   {
  19:     TBMessage.Text = ex.Message;
  20:   }
  21: }

Vorteile

Es wird die Ansicht nahezu 1:1 auch in Excel angezeigt. Dies betrifft auch eventuell vorhandene Gruppierungen und Aggregationsfunktionen. Der Arbeitsspeicherverbrauch ist gering, da die Daten ja bereits im DataGrid vorliegen und somit nicht nochmal aufbereitet werden müssen und auch XML erzeugt wird. Gute Performance auch bei größeren Datenmengen.

Nachteile

Das Speichern erfolgt in einem Excel-kompatiblen XML Format. Es erscheint beim Öffnen der Datei immer die Warnung, dass das Format nicht korrekt vorliegt. Excel ist aber in der Lage dann die Daten anzuzeigen. Da XML abgespeichert wird, ist die Dateigröße entsprechend groß. Im Zuge eines MVVM Pattern ist dies weniger zu gebrauchen, da die Methodik am Control RadGridView vorhanden ist. Es sollte dann schon im Code-Behind des UserControls implementiert werden.

Datensätze 100 1.000 10.000 100.000 1.000.000
Zuwachs
Arbeitsspeicher (K)
1.000 1.000 1.000 1.000 10.000
Zeit (s) <1 <1 <1 2-3 22-24
Dateigröße (KB) 28 271 2.710 27.182 272.785

Fazit

Für eine Export-Methode, die dem MVVM-Pattern möglichst nahe kommt, ist eine der ersten 3 Optionen die richtige Wahl. Ist der Export nur auf dem Client notwendig und nicht Server Szenarien zu berücksichtigen würde ich die PIA Option wählen. In einem Server-Szenario ist sicherlich eine Excel-freie Option, die bessere Wahl. Hier scheint EPPlus eine gute Option zu sein, jedoch habe ich bisher keine näheren Erfahrungen mit dieser Bibliothek gemacht, da ich während des Test zufällig auf diese gestoßen bin. Kommt ein „SnapShot“ der angezeigten Daten am ehesten der fachlichen Anforderung nahe und man kann zum Beispiel die Telerik Controls im Projekt nutzen, so diese Option eine für mich zu favorisierende Wahl.

Share |