Modello di progettazione rispettabile per rendere flessibili / testabili i moduli nodo?

6

Sto cercando di ottenere alcuni input da alcuni tester più esperti di quello che sono. :)

Sto cercando di rendere testabili i miei moduli di nodo, consentendo la dipendenza spying / stubbing / mocking senza la necessità di utilizzare una libreria magica come rewire o mockery per intercettare le mie istruzioni import / require del modulo.

Stavo pensando di utilizzare un approccio in base al quale creerei una funzione di fabbrica all'interno di ciascun modulo. La funzione factory accetta le dipendenze richieste per il modulo da esportare. L'esportazione predefinita chiamerebbe la funzione di fabbrica usando le istruzioni di importazione standard dal modulo. Esporterei anche la funzione di fabbrica stessa, consentendo ai miei test di iniettare qualsiasi dipendenza che preferiscono.

Ecco un esempio (completamente ideato e semplificato per evidenziare l'approccio alla progettazione):

import { foo } from './utils/foo';

// We export our factory, exposing it to consumers.
export const factory = (dependencies = {}) => {
  const {
    $foo = foo // defaults to standard imp if none provided.
  } = dependencies;  

  return function bar() {
    return $foo();
  }
}

// The default implementation, which would end up using default deps.
export default factory();

Pensieri? È troppo verboso? C'è un meccanismo migliore per raggiungere lo stesso? Sono un po 'preoccupato per le aggiunte alla piastra della caldaia che aggiungerò al mio progetto, ma forse ne vale la pena.

    
posta ctrlplusb 08.02.2016 - 12:17
fonte

2 risposte

4

In generale, qualcosa di simile è una buona idea. In particolare, non ho molta esperienza con JavaScript, ma ho un sacco di esperienza nel testare altre lingue. E lì ho scoperto che esponendo ganci di callback come questo è una tecnica estremamente utile per consentire l'iniezione di dipendenza senza modificare drasticamente l'architettura generale - infatti, ho recentemente scritto un article suggerendo questo stile di gestione delle dipendenze per le applicazioni C ++. Il mio flusso di lavoro è simile a questo:

  • trova le dipendenze pertinenti. Non tutte le dipendenze devono essere prese in giro, specialmente se possono essere testate in modo indipendente. Tuttavia, estrai quelli in cui desideri inserire dati, inserisci spie di test o prendi in giro operazioni costose.

  • descrive il servizio fornito dalla dipendenza. Probabilmente non stai utilizzando l'intera API fornita dalla dipendenza, ma solo un piccolo sottoinsieme. Quali sono i servizi forniti da quella dipendenza? Può essere sensato catturare questa descrizione in un semplice oggetto wrapper. Nel caso più semplice, la dipendenza è solo su una singola chiamata di metodo, nel caso più generale il servizio che si desidera estrarre è una funzione di fabbrica. Il problema con le fabbriche è che includi l'intera API del loro prodotto nella tua API (consulta la Legge di Demetra per una discussione di questo)

  • estrai la dipendenza in un callback estraibile. Il valore predefinito si limita a delegare alla tua dipendenza. Il modo in cui accedi a tale callback può dipendere da una varietà di fattori. Sono un fan di passare il callback in ogni funzione che dipende da esso come argomento opzionale, e l'utilizzo del callback predefinito è nessuno è fornito. Tuttavia, una variabile globale può anche funzionare. Un compromesso descrive esplicitamente l'intera API del tuo modulo come un oggetto (vedi anche il Modello di facciata). Questo evita la gestione delle dipendenze globali, ma lo rende effettivamente globale per il tuo modulo.

  • sostituisce la dipendenza nei test.

La principale differenza tra il mio approccio e il tuo codice è che eliminerei il generatore barFactory , anche se non sono sicuro di aver capito correttamente questo codice ES6. Nel semplice vecchio JavaScript con il modello del modulo rivelatore, includerei un oggetto che descriveva tutte le dipendenze nella facciata del modulo:

var module = (function(){
  var deps = {};
  deps.frobnicateMyThing = function(thing) {
    staticDependency.frobnicate(thing);
  };

  function externallyVisibleApi() {
    ...
    deps.frobnicateMyThing(thing);
  }

  return {
    "externallyVisibleApi": externallyVisibleApi,
    "deps": deps,
  };
})();

...
// in test:
module.deps.frobnicateMyThing = function(thing) {
  assertIGotTheExpected(thing);
};

module.externallyVisibleApi();
    
risposta data 08.02.2016 - 13:06
fonte
1

Dopo aver provato diversi approcci, mi piace scrivere moduli di nodo molto orientati agli oggetti. Praticamente preso in prestito da Java e simili, un file è un modulo è una classe.

// MyModule.js
class MyModule {

    // provide dependencies in constructor
    constructor(dependencyA, dependencyB) {
        this.dependencyA = dependencyA;
        this.dependencyB = dependencyB;
    }

}

module.exports = MyModule;


// MyModuleTest.js
var MyModule = require("./MyModule");

describe("MyModule Test", function () {

    it("should do something", function () {
        var mockA = {};
        var mockB = {};
        var myModule = new MyModule(mockA, mockB);

        // work with myModule and mocks
    });

});

Questo ha diversi vantaggi:

  • Semplifica il testing e il mocking
  • Puoi usare le dipendenze reali per i test di integrazione altrettanto facile
  • Non esiste uno stato globale nei test, puoi sempre lavorare con istanze fresche e oggetti separati come mock. L'approccio tradizionale in JavaScript è quello di creare stub o spie su oggetti "globali". Questo introduce lo stato globale, che è veramente pericoloso e soggetto a errori (nella mia esperienza).
  • È possibile utilizzare un contenitore IOC con l'iniezione del costruttore per la produzione

Un esempio del CIO con yaioc (disclaimer: I'm the author):

// myapp.js
var container = yaioc.container();
container.register(require("./DependencyA");
container.register(require("./DependencyB");
container.register(require("./MyModule"));
// ...
var myModule = container.get("myModule");

L'ho usato per diversi progetti e sono contento della grande flessibilità e semplicità dei test. C'è poco overhead e nessun complicato derisione / reset / magia da ricordare.

    
risposta data 25.02.2016 - 18:52
fonte

Leggi altre domande sui tag