Jak mogę makować zależności dla testów jednostkowych w RequireJS?

127

Mam moduł AMD, który chcę przetestować, ale zamiast ładować rzeczywiste zależności chcę wyśmiać jego zależności. Używam requirejs, a kod mojego modułu wygląda mniej więcej tak:

define(['hurp', 'durp'], function(Hurp, Durp) {
  return {
    foo: function () {
      console.log(Hurp.beans)
    },
    bar: function () {
      console.log(Durp.beans)
    }
  }
}

Jak mogę wyszydzać hurpi durpefektywnie testować jednostki?

jergason
źródło
Po prostu robię szalone rzeczy eval w node.js, aby wyszydzić definefunkcję. Jest jednak kilka różnych opcji. Opublikuję odpowiedź w nadziei, że będzie pomocna.
jergason
1
W przypadku testów jednostkowych z Jasmine możesz również rzucić okiem na Jasq . [Uwaga: utrzymuję bibliotekę]
biril,
1
Jeśli testujesz w węźle env, możesz użyć pakietu require-mock . To pozwala łatwo wyśmiewać swoich zależności, wymienić moduły itd. Jeśli potrzebujesz przeglądarki z obciążeniem env moduł asynchroniczny - można spróbować Squire.js
ValeriiVasin

Odpowiedzi:

65

Po przeczytaniu tego posta wymyśliłem rozwiązanie, które wykorzystuje funkcję konfiguracji requirejs do stworzenia nowego kontekstu dla twojego testu, w którym możesz po prostu mockować swoje zależności:

var cnt = 0;
function createContext(stubs) {
  cnt++;
  var map = {};

  var i18n = stubs.i18n;
  stubs.i18n = {
    load: sinon.spy(function(name, req, onLoad) {
      onLoad(i18n);
    })
  };

  _.each(stubs, function(value, key) {
    var stubName = 'stub' + key + cnt;

    map[key] = stubName;

    define(stubName, function() {
      return value;
    });
  });

  return require.config({
    context: "context_" + cnt,
    map: {
      "*": map
    },
    baseUrl: 'js/cfe/app/'
  });
}

Tworzy więc nowy kontekst, w którym definicje Hurpi Durpzostaną ustawione przez obiekty przekazane do funkcji. Math.random dla nazwy jest może trochę brudny, ale działa. Bo jeśli będziesz miał mnóstwo testów, musisz stworzyć nowy kontekst dla każdego pakietu, aby zapobiec ponownemu użyciu twoich mocków lub załadować mocks, gdy potrzebujesz prawdziwego modułu requirejs.

W twoim przypadku wyglądałoby to tak:

(function () {

  var stubs =  {
    hurp: 'hurp',
    durp: 'durp'
  };
  var context = createContext(stubs);

  context(['yourModuleName'], function (yourModule) {

    //your normal jasmine test starts here

    describe("yourModuleName", function () {
      it('should log', function(){
         spyOn(console, 'log');
         yourModule.foo();

         expect(console.log).toHasBeenCalledWith('hurp');
      })
    });
  });
})();

Więc używam tego podejścia w produkcji przez jakiś czas i jest naprawdę solidne.

Andreas Köberle
źródło
1
Podoba mi się to, co tutaj robisz ... zwłaszcza, że ​​możesz załadować inny kontekst dla każdego testu. Jedyne, co chciałbym zmienić, to to, że wygląda na to, że działa tylko wtedy, gdy wyśmiewam wszystkie zależności. Czy znasz sposób na zwrócenie pozorowanych obiektów, jeśli istnieją, ale powrót do pobierania z rzeczywistego pliku .js, jeśli nie podano makiety? Próbowałem przekopać się przez wymagany kod, aby to rozgryźć, ale trochę się gubię.
Glen Hughes
5
Wyśmiewa tylko zależność przekazaną do createContextfunkcji. Więc w twoim przypadku, jeśli przejdziesz tylko {hurp: 'hurp'}do funkcji, durpplik zostanie załadowany jako normalna zależność.
Andreas Köberle
1
Używam tego w Railsach (z jasminerice / phantomjs) i jest to najlepsze rozwiązanie, jakie znalazłem do kpiny z RequireJS.
Ben Anderson
13
+1 Niezbyt ładne, ale ze wszystkich możliwych rozwiązań ten wydaje się najmniej brzydki / niechlujny. Ten problem zasługuje na więcej uwagi.
Chris Salzberg
1
Aktualizacja: każdemu, kto rozważa to rozwiązanie, sugeruję sprawdzenie squire.js ( github.com/iammerrick/Squire.js ) wspomnianego poniżej. To fajna implementacja rozwiązania, które jest podobne do tego, tworząc nowe konteksty wszędzie tam, gdzie potrzebne są stuby.
Chris Salzberg,
44

możesz chcieć sprawdzić nową bibliotekę Squire.js

z dokumentów:

Squire.js to wstrzykiwacz zależności dla użytkowników Require.js, który ułatwia symulowanie zależności!

zniszczony
źródło
2
Zdecydowanie polecam! Aktualizuję swój kod, aby korzystał z squire.js i jak dotąd bardzo mi się to podoba. Bardzo prosty kod, bez wielkiej magii pod maską, ale zrobiony w sposób (stosunkowo) łatwy do zrozumienia.
Chris Salzberg,
1
Miałem wiele problemów z efektami ubocznymi giermka w innych testach i nie mogę tego polecić. Polecam npmjs.com/package/requirejs-mock
Jeff Whiting
17

Znalazłem trzy różne rozwiązania tego problemu, żadne z nich nie jest przyjemne.

Definiowanie zależności w tekście

define('hurp', [], function () {
  return {
    beans: 'Beans'
  };
});

define('durp', [], function () {
  return {
    beans: 'durp beans'
  };
});

require('hurpdhurp', function () {
  // test hurpdurp in here
});

Fugly. Musisz zaśmiecać swoje testy dużą ilością standardowych schematów AMD.

Ładowanie pozornych zależności z różnych ścieżek

Obejmuje to użycie oddzielnego pliku config.js do zdefiniowania ścieżek dla każdej z zależności, które wskazują na makiety zamiast oryginalnych zależności. Jest to również brzydkie, wymagające stworzenia wielu plików testowych i plików konfiguracyjnych.

Fake It In Node

To jest moje obecne rozwiązanie, ale nadal jest straszne.

Tworzysz własną definefunkcję, aby dostarczyć własne makiety do modułu i umieścić swoje testy w wywołaniu zwrotnym. Następnie evalmoduł do przeprowadzania testów, na przykład:

var fs = require('fs')
  , hurp = {
      beans: 'BEANS'
    }
  , durp = {
      beans: 'durp beans'
    }
  , hurpDurp = fs.readFileSync('path/to/hurpDurp', 'utf8');
  ;



function define(deps, cb) {
  var TestableHurpDurp = cb(hurp, durp);
  // now run tests below on TestableHurpDurp, which is using your
  // passed-in mocks as dependencies.
}

// evaluate the AMD module, running your mocked define function and your tests.
eval(hurpDurp);

To jest moje ulubione rozwiązanie. Wygląda trochę magicznie, ale ma kilka zalet.

  1. Uruchom testy w węźle, więc nie mieszaj do automatyzacji przeglądarki.
  2. Mniejsze zapotrzebowanie na niechlujne wzorce AMD w testach.
  3. Możesz użyć evalw gniewie i wyobrazić sobie Crockforda wybuchającego wściekłością.

Oczywiście nadal ma pewne wady.

  1. Ponieważ testujesz w węźle, nie możesz nic zrobić ze zdarzeniami przeglądarki ani manipulacją DOM. Dobry tylko do testowania logiki.
  2. Wciąż trochę niezdarny do skonfigurowania. Musisz mockować się definew każdym teście, ponieważ to właśnie tam są wykonywane testy.

Pracuję nad narzędziem do uruchamiania testów, aby podać ładniejszą składnię dla tego rodzaju rzeczy, ale nadal nie mam dobrego rozwiązania problemu 1.

Wniosek

Wyśmiewanie deps w requirejs jest do bani. Znalazłem sposób, który działa, ale nadal nie jestem z niego zbyt zadowolony. Daj mi znać, jeśli masz jakieś lepsze pomysły.

jergason
źródło
15

Jest config.mapopcja http://requirejs.org/docs/api.html#config-map .

Jak go używać:

  1. Zdefiniuj normalny moduł;
  2. Zdefiniuj moduł odgałęzienia;
  3. Skonfiguruj RequireJS w sposób jawny;

    requirejs.config({
      map: {
        'source/js': {
          'foo': 'normalModule'
        },
        'source/test': {
          'foo': 'stubModule'
        }
      }
    });
    

W takim przypadku dla normalnego i testowego kodu można użyć foomodułu, który będzie prawdziwym odniesieniem do modułu i odpowiednio będzie zastępował.

Artem Oboturov
źródło
To podejście zadziałało dla mnie naprawdę dobrze. W moim przypadku dodałem to do kodu HTML strony testowej -> map: {'*': {'Common / Modules / possibleModule': '/Tests/Specs/Common/usefulModuleMock.js'}}
Aligned
9

Możesz użyć testr.js do mockowania zależności. Możesz ustawić testr tak, aby ładował makiety zależności zamiast oryginalnych. Oto przykład użycia:

var fakeDep = function(){
    this.getText = function(){
        return 'Fake Dependancy';
    };
};

var Module1 = testr('module1', {
    'dependancies/dependancy1':fakeDep
});

Sprawdź również: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/

janith
źródło
2
Naprawdę chciałem, aby testr.js działał, ale wydaje mi się, że nie jest jeszcze do końca odpowiedni. Na koniec wybieram rozwiązanie @Andreas Köberle, które doda zagnieżdżone konteksty do moich testów (niezbyt ładne), ale konsekwentnie działa. Chciałbym, żeby ktoś mógł skupić się na rozwiązaniu tego rozwiązania w bardziej elegancki sposób. Będę oglądał testr.js i jeśli / kiedy zadziała, dokonam zmiany.
Chris Salzberg
@shioyama cześć, dzięki za informację zwrotną! Chętnie przyjrzę się, jak skonfigurowałeś testr.js w swoim stosie testowym. Chętnie pomożemy Ci rozwiązać wszelkie problemy, które możesz mieć! Jest też strona Github Issues, jeśli chcesz coś tam zarejestrować. Dzięki,
Matty F
1
@MattyF przepraszam, nawet nie pamiętam teraz, jaki dokładny powód był taki, że testr.js nie działał dla mnie, ale doszedłem do wniosku, że użycie dodatkowych kontekstów jest właściwie całkiem w porządku i faktycznie jest zgodne z tym, jak require.js miał być używany do mockowania / stubbowania.
Chris Salzberg,
2

Ta odpowiedź jest oparta na odpowiedzi Andreasa Köberle .
Wdrożenie i zrozumienie jego rozwiązania nie było dla mnie łatwe, więc wyjaśnię je bardziej szczegółowo, jak to działa, oraz kilka pułapek, których należy unikać, mając nadzieję, że pomoże to przyszłym odwiedzającym.

A więc przede wszystkim konfiguracja:
używam Karmy jako narzędzia do uruchamiania testów i MochaJ jako platformy testowej.

Używanie czegoś takiego jak Squire nie działało dla mnie, z jakiegoś powodu, kiedy go używałem, framework testowy wyrzucał błędy:

TypeError: Nie można odczytać wywołania właściwości o wartości undefined

RequireJs ma możliwość mapowania identyfikatorów modułów na inne identyfikatory modułów. Pozwala również na stworzenie requirefunkcji, która używa innej konfiguracji niż globalna require.
Te cechy są kluczowe, aby to rozwiązanie działało.

Oto moja wersja kodu próbnego, w tym (dużo) komentarzy (mam nadzieję, że jest to zrozumiałe). Owinąłem go wewnątrz modułu, aby testy mogły go łatwo wymagać.

define([], function () {
    var count = 0;
    var requireJsMock= Object.create(null);
    requireJsMock.createMockRequire = function (mocks) {
        //mocks is an object with the module ids/paths as keys, and the module as value
        count++;
        var map = {};

        //register the mocks with unique names, and create a mapping from the mocked module id to the mock module id
        //this will cause RequireJs to load the mock module instead of the real one
        for (property in mocks) {
            if (mocks.hasOwnProperty(property)) {
                var moduleId = property;  //the object property is the module id
                var module = mocks[property];   //the value is the mock
                var stubId = 'stub' + moduleId + count;   //create a unique name to register the module

                map[moduleId] = stubId;   //add to the mapping

                //register the mock with the unique id, so that RequireJs can actually call it
                define(stubId, function () {
                    return module;
                });
            }
        }

        var defaultContext = requirejs.s.contexts._.config;
        var requireMockContext = { baseUrl: defaultContext.baseUrl };   //use the baseUrl of the global RequireJs config, so that it doesn't have to be repeated here
        requireMockContext.context = "context_" + count;    //use a unique context name, so that the configs dont overlap
        //use the mapping for all modules
        requireMockContext.map = {
            "*": map
        };
        return require.config(requireMockContext);  //create a require function that uses the new config
    };

    return requireJsMock;
});

Największa pułapka natknąłem, który kosztował mnie dosłownie godzinami, było stworzenie config RequireJs. Próbowałem (głęboko) go skopiować i zastąpić tylko niezbędne właściwości (takie jak kontekst lub mapa). To nie działa! Skopiuj tylko baseUrl, to działa dobrze.

Stosowanie

Aby z niego skorzystać, wymagaj go w swoim teście, utwórz makiety, a następnie przekaż go createMockRequire. Na przykład:

var ModuleMock = function () {
    this.method = function () {
        methodCalled += 1;
    };
};
var mocks = {
    "ModuleIdOrPath": ModuleMock
}
var requireMocks = mocker.createMockRequire(mocks);

A tutaj przykład pełnego pliku testowego :

define(["chai", "requireJsMock"], function (chai, requireJsMock) {
    var expect = chai.expect;

    describe("Module", function () {
        describe("Method", function () {
            it("should work", function () {
                return new Promise(function (resolve, reject) {
                    var handler = { handle: function () { } };

                    var called = 0;
                    var moduleBMock = function () {
                        this.method = function () {
                            methodCalled += 1;
                        };
                    };
                    var mocks = {
                        "ModuleBIdOrPath": moduleBMock
                    }
                    var requireMocks = requireJsMock.createMockRequire(mocks);

                    requireMocks(["js/ModuleA"], function (moduleA) {
                        try {
                            moduleA.method();   //moduleA should call method of moduleBMock
                            expect(called).to.equal(1);
                            resolve();
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
            });
        });
    });
});
Domysee
źródło
0

jeśli chcesz wykonać zwykłe testy js, które izolują jedną jednostkę, możesz po prostu użyć tego fragmentu:

function define(args, func){
    if(!args.length){
        throw new Error("please stick to the require.js api which wants a: define(['mydependency'], function(){})");
    }

    var fileName = document.scripts[document.scripts.length-1].src;

    // get rid of the url and path elements
    fileName = fileName.split("/");
    fileName = fileName[fileName.length-1];

    // get rid of the file ending
    fileName = fileName.split(".");
    fileName = fileName[0];

    window[fileName] = func;
    return func;
}
window.define = define;
user3033599
źródło