Was sind - rein formal, nicht applikatorisch - die wesentlichen Charakteristika dieser Funktion?
function btn_vb_refresh() { var lSubpanel, lFcode = "", lDivId = "", lSubmit, lPages; if (!clientErrorCheck()) { return; } if (!check_page()) return; lSubpanel = getText("subpanel"); clearError(); lSubmit = false; if (byId('input_vb_filter')) { setText('vb_cur_filter',getText('input_vb_filter')); } if (getText("panel") == 'dlpview') { if (!lSubpanel) { lSubpanel = 'sum'; } lFcode = 'main__ajax_dlpview_refresh'; lDivId = "vorbest_" + lSubpanel; lPages = 0; if (byId("vb_total_pages")) { lPages = parseInt(getText("vb_total_pages"), 10); } if (lPages === 0) { lSubmit = true; } } else { if (!lSubpanel) { lSubpanel = 'vku'; } if (lSubpanel == 'vk') { lFcode = 'main__ajax_vk_refresh'; lDivId = "vorbest_vk"; } if (lSubpanel == 'vku') { lFcode = 'main__ajax_vku_refresh'; lDivId = "vorbest_vku"; } if (lSubpanel == 'dlp') { lFcode = 'main__ajax_dlp_refresh'; lDivId = "vorbest_dlp"; } } if (!lSubmit) { if (lDivId && byId(lDivId)) { lSubmit = false; } else { lSubmit = true; } } if (!lSubmit) { setCssForDatatableWidth(); call_ajax(lFcode, '#' + lDivId); } else { genericSubmit('main__vb_refresh'); } return; // sic! }Wir können folgende formalen Eigenschaften dieser Funktion festhalten:
- Die Funktion hat keine eigenen Aufrufparameter
- Sie verwendet nicht das this-Objekt
- Sie ruft andere Funktionen auf, die aber alle direkt global deklariert sind (keine Methoden eines Objekts)
- Die aufgerufenen Funktionen haben entweder gar keinen, einen oder zwei stringförmige Parameter
- Der Rückgabewert der aufgerufenen Funktionen wird entweder ignoriert oder auf Initialwert verglichen, oder wie ein String behandelt.
Wenn wir die Funktion nun auch inhaltlich genauer anschauen, so geht es offenbar um folgendes:
- Die aufgerufenen Funktionen entstammen, bis auf die Funktion call_ajax, meiner minimalistischen JavaScript-Bibliothek minlib.js.
- Die Funktion entscheidet anhand gewisser Kontextinformationen, ob ein Ajax-Request call_ajax oder ein genericSubmit ausgeführt werden soll.
- Im Ajax-Fall ermittelt die Funktion die beiden Aufrufparameter lFcode und '#' + lDivId
- Die aufgerufenen Funktionen müssen mitsamt der beim Aufruf übergebenen Parameter protokolliert werden, um die richtige Abfolge der Aufrufe verifizieren zu können.
- Die aufgerufenen Funktionen werden im globalen Kontext als Mocks definiert und können Werte zurückgeben, die den Verlauf der Funktion beeinflussen.
- Die einzelnen Testfälle können dann verifizieren, dass die Funktion, abhängig von den jeweiligen Testdaten, entweder die Funktion call_ajax mit den erwarteten Parametern, oder die Funktion genericSubmit mit dem angegebenen fixen Parameter main_vb_refresh aufruft.
Jeder einzelne Testfall stellt einen Ausführungspfad, d.h. eine bestimmte Kombination von Entscheidungen beim Durchlaufen des Codes dar. Nehmen wir folgenden Beispiel-Testfall:
- Die Funktion clientErrorCheck() liefert true zurück, was wohl bedeutet, dass die Daten keine Eingabefehler enthalten. Die Ausführung wird dann fortgesetzt.
- Auch die Funktion check_page() liefert true zurück, was entsprechend wohl bedeutet: die Daten "der angezeigten Seite" sind konsistent. Die Ausführung wird fortgesetzt.
- Das Feld subpanel möge den leeren String als Wert enthalten, getText('subpanel') liefert dann '' zurück, was als Wert der lokalen Variablen lSubpanel gespeichert wird.
- Das Feld panel enthalte den Wert 'dlpview', so dass die Ausführung in den if-Zweig der entsprechenden Abfrage läuft.
- Es existiere kein Feld mit der ID 'vb_total_pages'. Im Effekt wird die lokale Variable lPages auf 0 und somit lSubmit auf true gesetzt.
- Die Funktion entscheidet sich aufgrund dieser Vorgeschichte am Schluss, genericSubmit('main__vb_refresh') aufzurufen.
{ name:"dlpview: Submit if no pages", fixture:{ clientErrorCheck:true, check_page:true, getText:{ subpanel:'', panel:'dlpview' }, byId:{ vb_total_pages:null, } }, expected_history:[ { fname: 'genericSubmit', args: [ 'main__vb_refresh' ] } ], expected_rval:undefined // hier bedeutungslos, da die Funktion keine Rückgabewerte hat }Das zu schreibende Testmodul müsste als Interpreter für ein solches Testdatenformat fungieren:
- Für jeden Testfall, der in Form eines Objekts in obigem Format vorliegt, werden die Daten des Members fixture benutzt, um die Mockfunktionen geeignet vorzubelegen. Hierzu sind leider globale Variablen unvermeidlich, die Mockfunktionen müssen dem globalen Objekt global als Members hinzugefügt werden.
- Nun wird die Testfunktion aufgerufen. Bei jedem Funktionsaufruf innerhalb dieses Testaufrufs werden die Rückgabedaten gemäss fixture bedient, und der aktuelle Aufruf mitsamt seinen Argumenten fortgeschrieben.
- Nach dem Aufruf wird geprüft, ob die Geschichte sämtlicher Funktionsaufrufe, die die zu testende Funktion getätigt hat, den Array expected_history als echte geordnete Teilmenge enthält. expected_history muss also nicht die aktuelle Historie sämtlicher Funktionsaufrufe aufführen - das wäre zuviel Schreibarbeit, besser ist die Reduktion aufs Wesentliche. Vielmehr muss aus der aktuellen Historie der Funktionsaufrufe ableitbar sein, dass die Aufrufe der expected_history in der dort angegebenen Reihenfolge und mit den dort angegebenen Argumenten ausgeführt wurden.
- Es wird geprüft, ob die Funktion den erwarteten Rückgabewert expected_rval hat.
- Für jeden Testfall wird in einer Ergebniszeile mit dem Code ok oder not ok notiert, ob er bestanden wurde. Genauer gesagt, soll das Output dem einfachen Test Anything Protocol folgen.
function extend(destination, source) { for (var property in source) { if (typeof source[property] === "object" && source[property] !== null && destination[property]) { extend(destination[property], source[property]); } else { destination[property] = source[property]; } } return destination; }lässt sich der Definitionsschritt für das Mocking wie folgt ausführen:
// Overall Setup: Define all functions in a global namespace var fnames = {}; test.cases.forEach(function(testCase){ extend( fnames,Object.keys(testCase.fixture)) }); Object.keys(fnames).forEach( function(fname) { global[fname] = function() { return mock({ fname:fname, args:arguments }); } });Die Funktion mock() ist also eine allgemeine Umleitungsfunktion für alle Funktionsaufrufe, die in den Fixtures deklariert sind. Sie protokolliert den aktuellen Aufruf (Funktionsname und Argumente), und ermittelt dann den Rückgabewert aus der Fixture. Hierbei begnüge ich mich aktuell auf den Fall eines Aufrufs mit höchstens einem Argument (bei Bedarf lässt sich das auf beliebig viele Argumente erweitern):
function mock(func) { var fval,rval; if ((fval = _mock[func.fname])) { if (func.args.length === 0) rval = fval; else if (func.args.length == 1) rval = fval[func.args[0]]; } call_history.push(new FunctionCall(func.fname,func.args,rval)); return rval; }Nach diesen Vorbereitungen kann die eigentliche Testschleife laufen. Sie sollte nun keine Überraschungen mehr enthalten:
// Now process each individual test case test.cases.forEach( function(testCase, testCaseIndex) { var msg = ""; try { setup(testCase.fixture); rval = test.func(); assert_call_history(testCase.expected_history); assert_equals(testCase.expected_rval,rval); } catch (e) { msg = e; } console.log( result_line( testCase.name, testCaseIndex+1, msg) ); function result_line( name, index, msg) { var result = msg ? "NOT OK" : "OK" result += " " + index + " - " + name; if (msg) result += ": " + msg; return result; } });Wer sich für das vollständige JavaScript interessiert, notiert als node.js-Modul, kann es sich auf meinem pastebin ansehen.
Die verschiedenen Ausführungspfade sind mit den folgenden dreizehn Testfällen abgedeckt, die ich während der Bearbeitung der Funktion immer mitlaufen lasse:
1..13 ok 1 - stop if client error check fails ok 2 - stop if check_page fails ok 3 - dlpview: Submit if no pages ok 4 - dlpview: input_vb_filter will be transferred to vb_cur_filter if present ok 5 - dlpview: No submit if there are pages ok 6 - dlpview: Use 'sum' as default subpanel ok 7 - Not dlpview, subpanel vk: Submit if the div doesn't exist ok 8 - Not dlpview, subpanel vku: Submit if the div doesn't exist ok 9 - Not dlpview, no subpanel: Use 'vku' as default ok 10 - Not dlpview, subpanel dlp: Submit if the div doesn't exist ok 11 - Not dlpview, subpanel vk: Ajax call if the div exists ok 12 - Not dlpview, subpanel vku: Ajax call if the div exists ok 13 - Not dlpview, subpanel dlp: Ajax call if the div existsAm Ende hatte die Funktion die folgende Form.
function btn_vb_refresh() { if (!clientErrorCheck() || !check_page()) return; var panel = getText("panel"); var subpanel = getText("subpanel") || getDefaultSubpanel(); clearError(); // Inhalt des Eingabefilterfelds in ein benanntes Feld transportieren setText('vb_cur_filter',getText('input_vb_filter')); // Server-Request aktualisiert entweder nur Tabelle oder ganze Seite if (actualizeTableOnly( )) { doActualizeTable( ) } else { genericSubmit('main__vb_refresh'); } function actualizeTableOnly() { // Nur wenn Tabelle blätterbar und der Tabellen-Container existiert return (0 < parseInt(getText("vb_total_pages"), 10)) && byId(tableContainerId()); } function doActualizeTable() { // Tabelleninhalt per Ajax-Call aktualisieren setCssForDatatableWidth(); var view = (panel == 'dlpview' ? 'dlpview' : subpanel); var fcode = 'main__ajax_'+view+'_refresh'; call_ajax(fcode, '#' + tableContainerId()); } function tableContainerId( ) { return "vorbest_" + subpanel; } function getDefaultSubpanel() { return (panel == "dlpview" ? 'sum' : 'vku'); } }Die Verkleinerung der Zeilenzahl - neu sind es 48 statt vorher 77 Zeilen - stand nicht im Vordergrund. Eher ging es darum, die Frage "Was tut diese Funktion" klarer hervortreten zu lassen, und zwar durch den Code selbst, weniger durch Kommentare.
Um das Was mache ich vom Wie mache ich's besser zu trennen, sind kleine lokale (innere) Funktionen in JavaScript eine gute Idee: Im Hauptcode der Funktion steht der Name der inneren Funktion, der sagt, was gemacht wird. Die Implementierung, weiter hinten im Code, zeigt dann, wie es gemacht wird. So wird man beim Lesen des Hauptcodes nicht abgelenkt durch die vielen kleinen konkreten Implementierungsentscheidungen, mit denen die einzelnen Teilaufgaben gelöst wurden - nur bei Bedarf kann man in die Implementierung der lokalen Funktion hineinverzweigen.
Oft zeigt sich dann, dass eine Reihe dieser kleinen lokalen Funktionen verallgemeinerungswürdig sind, d.h. aus der Funktion herausgezogen werden können, da sie auch an anderen Stellen aufrufbar sind. Ein Kandidat hierfür ist in diesem Fall die Funktion getDefaultSubpanel()
Keine Kommentare :
Kommentar veröffentlichen