Testen des datenintensiven Codes mit Go, Teil 3

Überblick

Dies ist Teil drei von fünf einer Tutorialserie zum Testen von datenintensivem Code mit Go. In Teil zwei habe ich Tests mit einer echten In-Memory-Datenschicht basierend auf der beliebten SQLite durchgeführt. In diesem Lernprogramm werde ich einen Vergleich mit einer lokalen komplexen Datenschicht durchführen, die eine relationale Datenbank und einen Redis-Cache enthält.

Testen gegen eine lokale Datenschicht

Das Testen gegen eine In-Memory-Datenschicht ist großartig. Die Tests sind blitzschnell und Sie haben die volle Kontrolle. Manchmal müssen Sie jedoch näher an der tatsächlichen Konfiguration Ihrer Produktionsdatenschicht sein. Hier sind einige mögliche Gründe:

  • Sie verwenden bestimmte Details Ihrer relationalen Datenbank, die Sie testen möchten.
  • Ihre Datenschicht besteht aus mehreren interagierenden Datenspeichern.
  • Der getestete Code besteht aus mehreren Prozessen, die auf dieselbe Datenschicht zugreifen.
  • Sie möchten Ihre Testdaten mit Standardwerkzeugen aufbereiten oder beobachten.
  • Sie möchten keine dedizierte speicherinterne Datenschicht implementieren, wenn sich Ihre Datenschicht im Fluss befindet.
  • Sie möchten nur wissen, dass Sie mit Ihrer tatsächlichen Datenschicht testen.
  • Sie müssen mit vielen Daten testen, die nicht in den Speicher passen.

Ich bin sicher, es gibt andere Gründe, aber Sie können sehen, warum die Verwendung einer In-Memory-Datenschicht zum Testen in vielen Fällen nicht ausreichend ist.

OK. Wir möchten also eine tatsächliche Datenschicht testen. Wir wollen aber trotzdem so leicht und agil wie möglich sein. Dies bedeutet eine lokale Datenschicht. Hier sind die Vorteile:

  • Sie müssen nichts im Rechenzentrum oder in der Cloud bereitstellen und konfigurieren.
  • Sie brauchen sich keine Sorgen zu machen, dass unsere Tests die Produktionsdaten versehentlich beschädigen.
  • Sie müssen sich nicht mit anderen Entwicklern in einer gemeinsam genutzten Testumgebung abstimmen. 
  • Keine Langsamkeit über die Netzwerkanrufe.
  • Volle Kontrolle über den Inhalt der Datenschicht mit der Möglichkeit, jederzeit von vorne zu beginnen.  

In diesem Tutorial werden wir den Vorzug geben. Wir implementieren (sehr teilweise) eine hybride Datenschicht, die aus einer relationalen MariaDB-Datenbank und einem Redis-Server besteht. Dann verwenden wir Docker, um eine lokale Datenschicht aufzubauen, die wir in unseren Tests verwenden können. 

Verwendung von Docker zur Vermeidung von Installationsproblemen

Zuerst brauchen Sie natürlich Docker. Lesen Sie die Dokumentation, wenn Sie mit Docker nicht vertraut sind. Im nächsten Schritt erhalten Sie Bilder für unsere Datenspeicher: MariaDB und Redis. Ohne zu sehr ins Detail zu gehen, ist MariaDB eine großartige relationale Datenbank, die mit MySQL kompatibel ist, und Redis ist ein großartiger Speicher für die Speicherung von In-Memory-Schlüsseln (und vieles mehr).. 

> docker pull mariadb…> docker pull redis…> docker images REPOSITORY TAG BILD-ID ERSTELLT GRÖSSE mariadb latest 51d6a5e69fa7 vor 2 Wochen 402MB redis latest b6dddb991dfa vor 2 Wochen 107MB 

Nun, da wir Docker installiert haben und die Images für MariaDB und Redis haben, können wir eine docker-compose.yml-Datei schreiben, mit der wir unsere Datenspeicher starten. Nennen wir unsere DB "songify".

mariadb-songify: image: mariadb: Letzter Befehl:> --general-log --general-log-file = / var / log / mysql / query.log expose: - "3306" Ports: - "3306: 3306" - Umgebung : MYSQL_DATABASE: "songify" MYSQL_ALLOW_EMPTY_PASSWORD: "true" volume_from: - mariadb-data mariadb-data: image: mariadb: neueste volumes: - / var / lib / mysql enterypoint: / bin / bash redis: image: redis expose: - " 6379 "Ports: -" 6379: 6379 " 

Sie können Ihre Datenspeicher mit der starten Docker zusammenstellen Befehl (ähnlich wie vagrant up). Die Ausgabe sollte so aussehen: 

> docker-compose up Hybridtest_redis_1 wird gestartet ... Hybridtest_mariadb-data_1 wird gestartet ... Hybridtest_redis_1 wird gestartet Hybridtest_mariadb-data_1 wird gestartet ... abgeschlossen Hybridtest_mariadb-songify_1 wird gestartet ... * DB von der Festplatte geladen: 0,002 Sekunden redis_1 | * Bereit, Verbindungen anzunehmen… mariadb-songify_1 | [Anmerkung] mysqld: bereit für Verbindungen… 

Zu diesem Zeitpunkt verfügen Sie über einen voll ausgestatteten MariaDB-Server, der Port 3306 überwacht, und einen Redis-Server, der Port 6379 überwacht (beide sind die Standardports)..

Die hybride Datenschicht

Lassen Sie uns diese leistungsstarken Datenspeicher nutzen und unsere Datenschicht zu einer hybriden Datenschicht aufrüsten, die Songs in Redis pro Benutzer zwischenspeichert. Wann GetSongsByUser ()aufgerufen wird, überprüft die Datenschicht zunächst, ob Redis die Songs bereits für den Benutzer speichert. Wenn dies der Fall ist, geben Sie einfach die Songs von Redis zurück. Wenn dies nicht der Fall ist (Cache-Miss), werden die Songs von MariaDB abgerufen und der Redis-Cache gefüllt, sodass er für das nächste Mal bereit ist. 

Hier ist die Struktur- und Konstruktordefinition. Die Struktur behält einen DB-Handle wie zuvor und auch einen Redis-Client. Der Konstruktor stellt eine Verbindung zur relationalen Datenbank sowie zu Redis her. Es erstellt das Schema und wird nur dann erneut gelöscht, wenn die entsprechenden Parameter erfüllt sind. Dies ist nur für das Testen erforderlich. In der Produktion erstellen Sie das Schema einmal (ignorieren Schema-Migrationen)..

type HybridDataLayer struct db * sql.DB redis * redis.Client func NewHybridDataLayer (dbHost-Zeichenfolge, dbPort int, redisHost-Zeichenfolge, createSchema-Bool, clearRedis-Bool) (* HybridDataLayer, Fehler) dsn: = fmt.Sprintf ("root @ tcp (% s:% d) / ", dbHost, dbPort) if createSchema err: = createMariaDBSchema (dsn) if err! = null return nil, err db, err: = sql.Open (" mysql ", dsn + "desongcious? parseTime = true") if err! = nil return nil, err redisClient: = redis.NewClient (& redis.Options Addr: redisHost + ": 6379", Passwort: "", DB: 0, ) _, err = redisClient.Ping (). Ergebnis () if err! = nil return nil, err wenn clearRedis redisClient.FlushDB () return & HybridDataLayer db, redisClient, nil

Verwenden von MariaDB

MariaDB und SQLite unterscheiden sich etwas in Bezug auf DDL. Die Unterschiede sind klein, aber wichtig. Go hat kein ausgereiftes Cross-DB-Toolkit wie Pythons fantastische SQLAlchemy. Sie müssen es also selbst verwalten (nein, Gorm zählt nicht). Die Hauptunterschiede sind:

  • Der SQL-Treiber ist "github.com/go-sql-driver/mysql"..
  • Die Datenbank befindet sich nicht im Speicher, daher wird sie jedes Mal neu erstellt (drop and create).. 
  • Das Schema muss aus einem Teil unabhängiger DDL-Anweisungen bestehen, statt einer Zeichenfolge aller Anweisungen.
  • Die automatisch zunehmenden Primärschlüssel sind mit gekennzeichnet AUTO_INCREMENT.
  • VARCHAR anstatt TEXT.

Hier ist der Code:

func createMariaDBSchema (dsn string) error db, err: = sql.Open ("mysql", dsn) if err! = nil return err // Wiederherstellen von DB-Befehlen: = [] string "DROP DATABASE songify;", "CREATE DATABASE songify;", für _, s: = Bereich (Befehle) _, err = db.Exec (s) if err! = Nil return err // Erzeuge Schema db, err = sql.Open ("mysql", dsn + "songify? parseTime = true") if err! = nil return err schema: = [] string 'CREATE TABLE WENN NICHT EXISTEN (ID INTEGER PRIMARY KEY) AUTO_INCREMENT, url VARCHAR (2088) UNIQUE , Titel VARCHAR (100), Beschreibung VARCHAR (500)); ',' CREATE TABLE WENN NOT EXISTS-Benutzer (ID INTEGER PRIMARY KEY) AUTO_INCREMENT, Name VARCHAR (100), E-Mail VARCHAR (100) UNIQUE, registered_at TIMESTAMP, last_login TIMESTAMP); ', "CREATE INDEX user_email_idx ON Benutzer (E-Mail);",' CREATE TABLE WENN NICHT EXISTS-Label (ID INTEGER PRIMARY KEY AUTO_INCREMENT, Name VARCHAR (100) UNIQUE); ', "CREATE INDEX Label_Name_idx ON Label (Name);", 'CREATE TABLE WENN NICHT EXISTS ist. Label_song (label_id INTEGER NOT NULL REFE.) RENCES-Label (ID), song_id INTEGER NOT NULL REFERENCES-Song (ID), PRIMARY KEY (label_id, song_id)); ',' CREATE TABLE WENN NICHT EXISTS ist user_song (user_id INTEGER NICHT NULL REFERENCES-Benutzer (id), song_id INTEGER NOT NULL REFERENCES song (id), PRIMARY KEY (user_id, song_id)); ', für _, s: = range (schema) _, err = db.Exec (s) wenn err! = nil return err return nil 

Redis verwenden

Redis ist sehr einfach von Go zu verwenden. Die Client-Bibliothek "github.com/go-redis/redis" ist sehr intuitiv und folgt den Redis-Befehlen treu. Um beispielsweise zu testen, ob ein Schlüssel existiert, verwenden Sie einfach die Ausgänge () Methode des Redis-Clients, die einen oder mehrere Schlüssel akzeptiert und zurückgibt, wie viele davon vorhanden sind. 

In diesem Fall überprüfe ich nur einen Schlüssel:

 count, err: = m.redis.Exists (email) .Result () wenn err! = null return err

Testen des Zugriffs auf mehrere Datenspeicher

Die Tests sind eigentlich identisch. Die Schnittstelle hat sich nicht geändert, und das Verhalten hat sich nicht geändert. Die einzige Änderung besteht darin, dass die Implementierung jetzt einen Cache in Redis enthält. Das GetSongsByEmail () Methode ruft jetzt einfach auf refreshUser_Redis ().

func (m * HybridDataLayer) GetSongsByUser (u User) (Titel [] Song, Fehler) err = m.refreshUser_Redis (u.Email, & songs) return 

Das refreshUser_Redis () Diese Methode gibt die User-Songs von Redis zurück, falls vorhanden, und holt sie anderweitig aus MariaDB.

type Songs * [] Song func (m * HybridDataLayer) refreshUser_Redis (E-Mail-String, Out-Songs) error count, err: = m.redis.Exists (email) .Result () wenn err! = nil return err wenn Anzahl zurückgegeben wird == 0 err = m.getSongsByUser_DB (E-Mail, out) wenn err! = Nil return err für _, song: = range * out s, err: = serializeSong (song) wenn err! = Nil return err  _, err = m.redis.SAdd (email, s) .Result () wenn err! = nil return err return Mitglieder, err: = m.redis.SMembers (email) .Result () für _ , member: = Bereichsmitglieder song, err: = deserializeSong ([] byte (member)) wenn err! = nil return err * out = anhängen (* out, song) return out, nil 

Aus der Sicht der Testmethodik besteht hier ein kleines Problem. Wenn wir die abstrakte Datenschicht-Schnittstelle testen, haben wir keine Einsicht in die Datenschichtimplementierung.

Zum Beispiel ist es möglich, dass ein großer Fehler vorliegt, bei dem die Datenschicht den Cache vollständig überspringt und die Daten immer aus der Datenbank abruft. Die Tests werden bestanden, aber wir können nicht vom Cache profitieren. In Teil 5 werde ich über das Testen Ihres Cache sprechen, was sehr wichtig ist.  

Fazit

In diesem Lernprogramm wurde das Testen mit einer lokalen komplexen Datenschicht beschrieben, die aus mehreren Datenspeichern besteht (relationale Datenbank und Redis-Cache). Wir haben Docker auch verwendet, um mehrere Datenspeicher zum Testen einfach bereitzustellen.

In Teil vier werden wir uns auf Tests mit entfernten Datenspeichern konzentrieren, Snapshots von Produktionsdaten für unsere Tests verwenden und auch unsere eigenen Testdaten generieren. Bleib dran!