thomaskekeisen.de

Aus dem Leben eines Bildschirmarbyters

Vorwort

Wie schon in meinem Artikel zum Update auf Version 1.2 dieser Webseite angekündigt, möchte ich hier ein paar Worte über die Verwendung von automatischen Tests in Verbindung mit gulp verlieren. Um meinen Blog zu testen verwende ich protractor, karma und gulp-expect-file. In diesem Artikel erkläre ich, wie ich gulp-expect-file- genau genommen meinen Fork von gulp-expect-file- verwende und somit derzeit über 6400 Fehlerszenarien automatisch überprüfe.

Wer sich die Dokumentation von gulp-expect-file anschaut stellt schnell fest, dass sich das Modul eigentlich nur dazu eignet, zu überprüfen, ob im dist -Ordner auch wirklich nur Dateien sind, die dort hingehören. Ich habe das Modul aber so erweitert, dass sich damit auch relativ einfache Unit-Tests abbilden lassen, ohne zusätzliche Frameworks mitschleppen zu müssen. So mache ich es mir zu Nutzen, dass gulp-expect-file die Möglichkeit bietet, einen sogenannten callback implementieren zu können, der letztlich eigene Prüflogik enthalten kann und damit theoretisch jedes Szenario automatisch testen kann.

Grundfunktion erklärt

Der Beispielcode anbei erklärt eigentlich schon relativ genau, wie ich gulp-expect-file verwende. Mit gulp.src(['version.json']) selektiere ich die Dateien, die ich testen möchte und als callback für plugins.expectFile übergebe ich eine Funktion, die pro gefundene Datei ein Mal aufgerufen wird. Gebe ich in dieser Funktion true zurück, scheint alles in Ordnung. Gebe ich aber false oder gar einen Fehlertext zurück, wird entsprechend ein zu behebender Fehler dokumentiert. Im Code anbei überprüfe ich beispielsweise, ob meine version.json auch wirklich das Wort "version" beinhaltet.

Screenshot
        
            var fs      = require('fs');
            var plugins = require('gulp-load-plugins')();

            gulp.task('run-tests', function() {
                gulp.src(['version.json'])
                    .pipe(plugins.expectFile(function (file) {
                        var fileContent = fs.readFileSync(file.path, 'utf8');

                        if (fileContent.indexOf('version') == -1) {
                            return 'missing "version" in file';
                        }

                        return true;
                    }))
                ;
            });
        
    

Meine Implementierung

Meine Test-Datei ist sehr schnell gewachsen und wurde irgendwann sehr unübersichtlich. Letztlich fühlte ich mich gezwungen, das Format dieser Datei zu ändern. Das Ergebnis ist jetzt eine relativ wartbare Datei, in der alle Tests sauber ausgelagert sind und ich neue Tests einfach "anmelden" kann. Anbei beispielhaft mein Test, der sicherstellt, dass jeder Blog-Beitrag auch einen Kommentare-Bereich enthält.

        
            var test = {};

            test.blogPostHasCommentsArea = {
                ignore: [
                    '/archiv/index.html',
                    '/archive/index.html'
                ],
                test: function (filePath, fileContent, test)
                      {
                          if (fileContent.indexOf('id="language-fallback"') == -1 &&
                              fileContent.indexOf('control-disqus-comments') == -1) {
                              return 'not containing a comments area';
                          }

                          return true;
                      }
            };
        
    

Außerdem habe ich auch das Auswählen der Dateien umstrukturiert, sodass ich relativ leicht eine Auswahl von auszuführenden Tests einer Liste von Dateien zuordnen kann. Im Bereich glob kann ich eine beliebig lange Liste von Dateinamen-Suchmustern übergeben, im tests -Bereich dann eine Liste von zuvor implementierten Tests, die auf jede einzelne gefundene Datei ausgeführt werden sollen.

        
            var filePairs = [
                {
                    glob: [
                        'dist/*/blog/**/*.html'
                    ],
                    tests: [
                        test.blogPostHasCommentsArea,
                        test.blogPostHasSocialMediaBox
                    ]
                }
            ];
        
    

Das war alles. Ich habe natürlich viel mehr Tests als hier beispielhaft genannt. Der restliche gulp-Task sieht wie folgt aus und erledigt das Einlesen der Dateien sowie das Ausführen der Tests vollautomatisch. Selbst das nachgelagerte Dekodieren von JSON wird unterstützt. Damit gulp-expect-file die Übergabe der Option summaryCallback unterstützt muss meinen Fork von gulp-expect-file durch das Eintragen von "gulp-expect-file": "https://github.com/blaues0cke/gulp-expect-file" in der package.json verwendet werden. Ebenfalls wird das npm-Modul merge2 benötigt um alle erzeugten Test-Streams in einen einzelnen, endgültigen Stream zu übergeben, mit dem gulp letztlich wieder umgehen kann.

        
            var fs      = require('fs');
            var plugins = require('gulp-load-plugins')();

            gulp.task('run-tests', function() {
                var errorBuffer = [];

                var errorCallback = function (error) {
                    errorBuffer.push(error);
                };

                var summaryBuffer = {
                    tests:    0,
                    passes:   0,
                    failures: 0
                };

                var summaryCallback = function (numTests, numPasses, numFailures) {
                    summaryBuffer.tests    += numTests;
                    summaryBuffer.passes   += numPasses;
                    summaryBuffer.failures += numFailures;
                };

                var options = {
                    errorOnFailure:  true,
                    hideSummary:     true,
                    silent:          false,
                    summaryCallback: summaryCallback
                };

                var stream = plugins.merge2();

                for (var filePairKey in filePairs) {
                    var currentFilePair = filePairs[filePairKey];

                    (function(currentFilePair) {
                        for (var globKey in currentFilePair.glob) {
                            var currentGlob = currentFilePair.glob[globKey];

                            (function (currentGlob) {
                                glob.sync(currentGlob).forEach(function(globFilePath) {
                                    for (var testKey in currentFilePair.tests) {
                                        var currentTest = currentFilePair.tests[testKey];

                                        (function(currentTest) {
                                            var newStream = gulp.src(globFilePath)
                                                .pipe(plugins.expectFile(options, function (filePath) {
                                                    var stats       = fs.statSync(filePath.path);
                                                    var fileContent = null;
                                                    var skipFile    = false;
                                                    var testResult  = true;

                                                    if (stats.isFile()) {
                                                        fileContent = fs.readFileSync(filePath.path, 'utf8');

                                                        if (currentTest.transform) {
                                                            if (currentTest.transform == 'json') {
                                                                fileContent = JSON.parse(fileContent);
                                                            }
                                                        }
                                                    }

                                                    if (currentTest.ignore) {
                                                        for (var ignoreKey in currentTest.ignore) {
                                                            var currentIgnorePath = currentTest.ignore[ignoreKey];

                                                            if (filePath.path.indexOf(currentIgnorePath) > -1) {
                                                                skipFile = true;

                                                                break;
                                                            }
                                                        }
                                                    }

                                                    if (!skipFile) {
                                                        testResult = currentTest.test(filePath, fileContent, currentTest);

                                                        if (testResult !== true) {
                                                            return testResult;
                                                        }
                                                    }

                                                    return true;
                                                }).on('error', errorCallback))
                                            ;

                                            stream.add(newStream.on('error', errorCallback));
                                        })(currentTest);
                                    }
                                });
                            })(currentGlob);
                        }
                    })(currentFilePair);
                }

                var fileExistanceTests = gulp.src(['dist/**/*'])
                    .pipe(plugins.expectFile(options, [
                        // Eine List evon Dateien die im dist-Ordner sein dürfen.
                        // Alle anderen Dateien erzeugen ebenfalls einen Fehler.
                        'dist/404.php'
                    ]).on('error', errorCallback))
                ;

                stream.add(fileExistanceTests.on('error', errorCallback));

                stream.add(gulp.src(['dist/index.php'])
                    .pipe(plugins.expectFile(options, [function(test) {
                        setTimeout(function() {
                            plugins.util.log(
                                'Tested',
                                plugins.util.colors.cyan(summaryBuffer.tests),    'tests,',
                                plugins.util.colors.cyan(summaryBuffer.passes),   'passes,',
                                plugins.util.colors.cyan(summaryBuffer.failures), 'failures:',
                                (summaryBuffer.failures > 0 ? plugins.util.colors.bgRed.white('FAIL') : plugins.util.colors.green('PASS'))
                            );
                        }, 1000);
                    }]))
                );

                return stream;
            });
        
    

Der letzte Stream für die Datei dist/index.php ist ein kleiner Workaround. Da ich für jedes Set von Dateien einzelne Streams erzeuge ( var newStream = gulp.src(globFilePath) ) muss ich dieser letztlich wieder zusammenführen. gulp-expect-file unterstützt das aber nur indirekt. Darum habe ich das ganze Modul so erweitert, dass ein optionaler Aufruf als summaryCallback übergeben werden kann. Dort zähle ich die erfolgreichen und fehlgeschlagenen Tests und zeige dann nach Ausführung aller Tests und nach einer weiteren gewarteten Sekunde die selbst generierte Zusammenfassung an. Würde ich das nicht tun, hätte ich über 6400 einzelne Konsolenausgaben und ich würde vermutlich nicht mal bemerken, dass ein Test fehlgeschlagen ist.

Teilen

Kommentare