Mam następujące moduły ES6:
network.js
export function getDataFromServer() {
return ...
}
widget.js
import { getDataFromServer } from 'network.js';
export class Widget() {
constructor() {
getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
render() {
...
}
}
Szukam sposobu na przetestowanie Widget za pomocą próbnej instancji getDataFromServer
. Gdybym użył oddzielnych <script>
modułów zamiast modułów ES6, jak w Karmie, mógłbym napisać swój test tak:
describe("widget", function() {
it("should do stuff", function() {
let getDataFromServer = spyOn(window, "getDataFromServer").andReturn("mockData")
let widget = new Widget();
expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
expect(otherStuff).toHaveHappened();
});
});
Jeśli jednak testuję moduły ES6 indywidualnie poza przeglądarką (jak z Mocha + babel), napisałbym coś takiego:
import { Widget } from 'widget.js';
describe("widget", function() {
it("should do stuff", function() {
let getDataFromServer = spyOn(?????) // How to mock?
.andReturn("mockData")
let widget = new Widget();
expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
expect(otherStuff).toHaveHappened();
});
});
OK, ale teraz getDataFromServer
nie jest dostępny w window
(cóż, w ogóle nie ma window
) i nie znam sposobu, aby wstrzyknąć rzeczy bezpośrednio do widget.js
własnego zakresu.
Więc dokąd mam się udać?
- Czy istnieje sposób uzyskania dostępu do zakresu
widget.js
lub przynajmniej zastąpienia jego importu własnym kodem? - Jeśli nie, w jaki sposób mogę uczynić
Widget
możliwym do przetestowania?
Rzeczy, które rozważałem:
za. Ręczne wstrzykiwanie zależności.
Usuń wszystkie importy z widget.js
i oczekuj, że wywołujący dostarczy deps.
export class Widget() {
constructor(deps) {
deps.getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
}
Nie podoba mi się zepsucie publicznego interfejsu Widgeta w ten sposób i ujawnianie szczegółów implementacji. Nie idź.
b. Ujawnij importy, aby umożliwić ich kpienie.
Coś jak:
import { getDataFromServer } from 'network.js';
export let deps = {
getDataFromServer
};
export class Widget() {
constructor() {
deps.getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
}
następnie:
import { Widget, deps } from 'widget.js';
describe("widget", function() {
it("should do stuff", function() {
let getDataFromServer = spyOn(deps.getDataFromServer) // !
.andReturn("mockData");
let widget = new Widget();
expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
expect(otherStuff).toHaveHappened();
});
});
Jest to mniej inwazyjne, ale wymaga ode mnie napisania wielu schematów dla każdego modułu i nadal istnieje ryzyko, że będę go używać getDataFromServer
zamiast tego przez deps.getDataFromServer
cały czas. Niepokoi mnie to, ale jak dotąd to mój najlepszy pomysł.
createSpy
( github.com/jasmine/jasmine/blob/… ) z importowanym odniesieniem do getDataFromServer z modułu „network.js”. Tak więc w pliku testów widżetu zaimportowałbyś getDataFromServer, a następnielet spy = createSpy('getDataFromServer', getDataFromServer)
spyOn
na tym obiekcie zaimportowanym znetwork.js
modułu. Jest to zawsze odniesienie do tego samego obiektu.Widget
publiczny interfejs?Widget
jest pomieszany bezdeps
. Dlaczego nie wyrazić zależności w sposób jawny?Odpowiedzi:
Zacząłem stosować
import * as obj
styl w moich testach, który importuje wszystkie eksporty z modułu jako właściwości obiektu, z którego można następnie mockować. Uważam, że jest to o wiele czystsze niż użycie czegoś takiego jak rewire lub proxyquire lub inna podobna technika. Robiłem to najczęściej, gdy potrzebowałem na przykład kpić z akcji Redux. Oto, czego mógłbym użyć w powyższym przykładzie:Jeśli twoja funkcja jest domyślnym eksportem, to
import * as network from './network'
utworzy{default: getDataFromServer}
i możesz mockować network.default.źródło
import * as obj
jedynego w teście, czy też w swoim zwykłym kodzie?[method_name] is not declared writable or has no setter
co ma sens, ponieważ import es6 jest stały. Czy istnieje sposób obejścia tego problemu?import
(w przeciwieństwie do tegorequire
, który może dotrzeć wszędzie) jest podnoszony, więc nie można technicznie importować wiele razy. Wygląda na to, że ktoś dzwoni do twojego szpiega? Aby zapobiec zepsuciu testów (tzw. Testowym zanieczyszczeniom), możesz zresetować swoich szpiegów w afterEach (np. Sinon.sandbox). Uważam, że Jasmine robi to automatycznie.import
w swoim JS nie używa tak naprawdę modułów ES6. Coś takiego jak webpack lub babel wkracza w czasie kompilacji i konwertuje je albo na swój własny wewnętrzny mechanizm do wywoływania odległych części kodu (np.__webpack_require__
), Albo w jeden z de facto standardów sprzed ES6 , CommonJS, AMD lub UMD. A ta konwersja często nie jest ściśle zgodna ze specyfikacją. Tak więc dla wielu, wielu programistów w tej chwili ta odpowiedź działa dobrze. Na razie.@carpeliam jest poprawne, ale pamiętaj, że jeśli chcesz szpiegować funkcję w module i użyć innej funkcji w tym module wywołującej tę funkcję, musisz wywołać tę funkcję jako część przestrzeni nazw eksportu, w przeciwnym razie szpieg nie będzie używany.
Błędny przykład:
Właściwy przykład:
źródło
exports.myfunc2
jest bezpośrednim odniesieniem domyfunc2
do momentu, w którymspyOn
zastępuje go odniesieniem do funkcji szpiegowskiej.spyOn
zmieni wartośćexports.myfunc2
i zastąpi go obiektem szpiegowskim,myfunc2
pozostając nietkniętym w zakresie modułu (ponieważspyOn
nie ma do niego dostępu)*
zamrożeniem obiektu, a atrybuty obiektu nie mogą być zmienione?export function
razem zexports.myfunc2
technicznie polega na mieszaniu składni modułu commonjs i ES6 i jest to niedozwolone w nowszych wersjach pakietu webpack (2+), które wymagają użycia składni modułu ES6 typu wszystko albo nic. Poniżej dodałem odpowiedź w oparciu o tę, która będzie działać w ścisłych środowiskach ES6.Zaimplementowałem bibliotekę, która próbuje rozwiązać problem mockowania w czasie wykonywania importu klas Typescript bez potrzeby, aby oryginalna klasa wiedziała o jakimkolwiek jawnym wstrzyknięciu zależności.
Biblioteka używa
import * as
składni, a następnie zastępuje oryginalny wyeksportowany obiekt klasą pośredniczącą. Zachowuje bezpieczeństwo typów, więc testy zostaną przerwane w czasie kompilacji, jeśli nazwa metody została zaktualizowana bez aktualizacji odpowiedniego testu.Tę bibliotekę można znaleźć tutaj: ts-mock-Import .
źródło
Odpowiedź @ vdloo sprawiła, że poszedłem we właściwym kierunku, ale używanie razem słów kluczowych commonjs „export” i „export” modułu ES6 w tym samym pliku nie zadziałało (narzeka webpack v2 lub nowszy). Zamiast tego używam domyślnego (nazwanej zmiennej) eksportu opakowującego wszystkie poszczególne eksporty z nazwanego modułu, a następnie importuję domyślny eksport w moim pliku testów. Używam następującej konfiguracji eksportu z mokką / sinonem, a stubbing działa dobrze bez konieczności ponownego okablowania itp .:
źródło
let MyModule
nie jest wymagane użycie domyślnego eksportu (może to być surowy obiekt). Ponadto ta metoda nie wymagamyfunc1()
dzwonieniamyfunc2()
, działa tylko po to, aby ją bezpośrednio szpiegować.Zauważyłem, że ta składnia działa:
Mój moduł:
Kod testowy mojego modułu:
Zobacz doc .
źródło
jest.mock()
musi odpowiadać nazwie użytej w import / packge.json zamiast nazwy stałej. W dokumentach oba są takie same, ale z kodem jakimport jwt from 'jsonwebtoken'
trzeba ustawić makietę jakjest.mock('jsonwebtoken')
Sam tego nie próbowałem, ale myślę, że kpina może działać. Pozwala na zastąpienie prawdziwego modułu dostarczonym przez Ciebie makietą. Poniżej znajduje się przykład, który daje wyobrażenie o tym, jak to działa:
Wygląda na
mockery
to, że nie jest już obsługiwany i myślę, że działa tylko z Node.js, ale nie mniej jednak jest to zgrabne rozwiązanie do wyszydzania modułów, które inaczej są trudne do podrobienia.źródło