Prywatność przez domknięcia a testy jednostkowe

1

Zastanawiam się nad podejściem do testowania prywatnych części kodu (przykłady w JS, ale dotyczy to każedgo języka obsługującego domknięcia).

Załóżmy, że mamy taki moduł (tak by wyglądał gdybym nie myślał o testach):

/**
* @module doSomething
*/

function doSomething(arg) {
  // code...
  const partialResult1 = createPartialResult1(someArg);
  const partialResult2 = createPartialResult2(anotherArg);
  // code ...

  return result;
}

function createPartialResult1(arg) {
  // code...

  return part;
}

function createPartialResult2(arg) {
  // code...

  return part;
}

export default doSomething;

Mam upublicznioną tylko jedną funkcję - doSomething, pozostałe funkcje są niewidoczne dla kodu spoza modułu - wg mnie tak właśnie powinno być, wystawiam minimalny potrzebny interfejs.

No tu pojawia się problem, niektóre funkcje prywatne są na tyle skomplikowane, że chciałbym je przetestować (realny przykład nad którym teraz pracuję to funkcja parsująca zapytanie JSON do AST, funkcje prywatne tworzą poszczególne segmenty AST + helpery). No i widzę kilka opcji:

  1. Mimo wszystko nie testować prywatnych funkcji - dla mnie odpada, mimo że jest to niby szczegół implementacyjny to takie testy znacząco ułatwiają mi pisanie kodu.

  2. Wyciągnąć prywatne funkcje do osobnego modułu i upublicznić je:

 /**
* @module partsCreators
*/

function createPartialResult1(arg) {
  // code...

  return part;
}

function createPartialResult2(arg) {
  // code...

  return part;
}

export {
  createPartialResult1,
  createPartialResult2,
};
/**
* @module doSomething
*/

import {
  createPartialResult1,
  createPartialResult2,
} from 'partsCreators';

function doSomething(arg) {
  // code...
  const partialResult1 = createPartialResult1(someArg);
  const partialResult2 = createPartialResult2(anotherArg);
  // code ...

  return result;
}

export default doSomething;

Mogę je teraz sobie łatwo przetestować, ale boli mnie trochę taka zmiana kodu pod testy, no ale aktualnie stosuję właśnie to podejście, ze względu na jego prostotę.

  1. Pozostawienie funkcji jako prywatnych + testy do nich. Jako że mamy domknięcie, to nie dobiorę się do funkcji prywatnych przez żadną refleksję itp, ale mogę warunkowo upublicznić elementy na cele testów, używając np plugina do Babela:
 /**
* @module doSomething
*/

function doSomething(arg) {
  // code...
  const partialResult1 = createPartialResult1(someArg);
  const partialResult2 = createPartialResult2(anotherArg);
  // code ...

  return result;
}

function createPartialResult1(arg) {
  // code...

  return part;
}

function createPartialResult2(arg) {
  // code...

  return part;
}

// It will be stripped by Babel if not running by testing tool
export const __test__ = {
  createPartialResult1,
  createPartialResult2,
};

export default doSomething;

Co powinno (wg mnie) być prywatne, pozostaje prywatne, ale dochodzi dodatkowy etap transpilacji kodu (lub w ogóle dochodzi transpilacja, jeśli wcześniej nie była potrzebna).

Którą metodę byście zastosowali? Może macie jeszcze jakieś inne pomysły?

5

Generalnie 2). Jeśli jakaś funkcja jest na tyle skomplikowana ze chciałbyś dla niej zrobić osobne testy jednostkowe to jest to przesłanka za tym że powinna być publicznym zachowaniem jakiegoś pod-modułu.

1

Z tego co wiem w React'owym Community popularne jest robienie czegoś takiego (prawie to samo co proponujesz jako rozwiązanie nr 3):

// funkcja prywatna, export tylko do testów
export function a() {

}

// funkcja prywatna, export tylko do testów
export function b() {

}

// funkcja publiczna, dlatego export default
export default function c() {
  a();
  b();
}

Za mało jeszcze odrosłem od ziemi, żeby wydać fachową opinię, ale jest to jakieś wyjście. Słyszałem, że to antipattern mieć defaultowy export i - nazwijmy to - normalne exporty, ale z drugiej strony.. Można to połączyć z process.env i exportować te funkcje tylko jeżeli odpalamy kod w środowisku testowym.

Innym dosyć ciekawym rozwiązaniem jest użycie rewire. Mocking Dependencies using rewire. Działa to banalnie prosto:

// foo.js
function doSomething() {
  console.log('Hello from doSomething')
}

function privateOne() {
  console.log('Hello from privateOne')
}

function privateTwo() {
  console.log('Hello from privateTwo')
}

module.exports = doSomething;
// tests/foo.js
var rewire = require('rewire'),
  foobar = rewire('./tests/foo'),
  privateOne = foobar.__get__('privateOne'),
  privateTwo = foobar.__get__('privateTwo');

foobar();
privateOne();
privateTwo();

I output:

$ node tests/foo.js
Hello from doSomething
Hello from privateOne
Hello from privateTwo
4

Chciałem napisać, żebyś podzielił to na dwa pliki (jeden plik z publicznym API, drugi plik prywatny), ale w zasadzie to samo napisałeś w punkcie 2.
Ale wrzucając swoje 2 grosze to wydaje mi się, że określenie "prywatne" jest dość względne. Coś może być prywatne z perspektywy użytkownika biblioteki (osoby, który robi npm install i korzysta z udostępnionego przez ciebie API) a jednak publiczne dla innych modułów w twojej bibliotece.

Jak patrzę po bibliotekach open source to często jest tam taki pattern, że są rzeczy rozdzielone. Moduł, który naprawdę definiuje jakieś funkcje to niekoniecznie ten, który je udostępnia światu jako publiczne API.

(chociaż publiczne/prywatne API tez jest dość względne, nic nie jest prywatne w JS, równie dobrze ktoś może zrobić tak require('biblioteka/lib/private/bla.js') i będzie mial ten moduł - no ale to jego sprawa wtedy, jak sobie zrobi kuku korzystając z prywatnego API).

(realny przykład nad którym teraz pracuję to funkcja parsująca zapytanie JSON do AST, funkcje prywatne tworzą poszczególne segmenty AST + helpery). No i widzę kilka opcji:

tj. piszesz parser JSONa?
swoją drogą szukałem czegoś takiego (JSON.parse nie zwraca pozycji w pliku na której są dane właściwości, więc jest dość bezużyteczny w pewnych zastosowaniach)

BTW fajne podejście ma Wujek Bob do tego:
https://8thlight.com/blog/uncle-bob/2015/06/30/the-little-singleton.html

"Tests trump Encapsulation.

What does that mean?
That means that tests win. No test can be denied access to a variable simply to maintain encapsulation."

3

Jeżeli Twoją metodą prywatną jest konwersja danych między formatami - to jest to już zupełnie niezależny moduł aplikacji - bo nie jest związany z żadnym obiektem/danymi/cokolwiek. Zrób dokładnie tak jak napisałeś w 2), wydziel moduł, wyeksportuj, testuj i w swoim module importuj i używaj :)

0

@Desu
To Rewire jest ciekawe, natomiast nie zastosuję go ze wzgledu na to, że używam ES6 modules.

Co do tego artykułu: https://howtonode.org/mocking-private-dependencies-using-rewire

Zamiast tego kodu (i całej magii Rewire):

// controllers.js:
var userService = require('./userService');

exports.allUsers = function (req, res, next) {
  userService.getUsers(/* some callback */);
};
// app.js:
var allUsers = require('./controllers').allUsers;

app
  .get('/users', allUsers);

chyba i tak wolałbym zrobić to w ten sposób (oczywiście promise zamiast callbacka):

// controllers.js:
export const allUsers = userService => (req, res, next) => {
  userService.getUsers(/* some callback */);
};
// app.js:
import { allUsers } from './controllers';
import userService from './userService';

app
  .get('/users', allUsers(userService));

i z mockowaniem też nie ma problemu.


@LukeJL
Przez prywatne miałem tu na myśli niedostpne spoza modułu.

Nie, nie piszę niestety parsera JSONa w takim sensie, o który Ci chyba chodzi - przekształcam query language w formacie JSON, nie robię odpowiednika JSON.parse.

Dzięki za przypomnienie tekstu Uncle Boba - kiedyś go czytałem, ale mi wyparował ;)


@dzek69
No główna funkcja jest oczywiście upubliczniona, zastanawiałem się, jak nie upubliczniać funkcji pomocniczych - one nie są używane nigdzie indziej niż poza funkcją główną, nawet chyba w JSDocu oznaczę je jako @private, żeby mi się nie generowała z nich dokumentacja (nawet gdy mam je w osobnym module).


Widać za dużo chcę na raz ;) Chyba nie będę się wygłupiał i zostawię tak jak jest (opcja 2), ale jak ktoś jeszcze ma jakieś wnioski to zapraszam - przyda się na przyszłość.

No i dziękuję za dotychczasowe odpowiedzi ;)

2
LukeJL napisał(a):

Ale wrzucając swoje 2 grosze to wydaje mi się, że określenie "prywatne" jest dość względne. Coś może być prywatne z perspektywy użytkownika biblioteki (osoby, który robi npm install i korzysta z udostępnionego przez ciebie API) a jednak publiczne dla innych modułów w twojej bibliotece.

Języki, które zostały zaprojektowane przed implementacją, z łatwością rozróżniają takie metody/klasy.

BTW fajne podejście ma Wujek Bob do tego:
https://8thlight.com/blog/uncle-bob/2015/06/30/the-little-singleton.html

"Tests trump Encapsulation.

What does that mean?
That means that tests win. No test can be denied access to a variable simply to maintain encapsulation."

To nie jest "fajne podejście" tylko starcza demencja.
Hermetyzacja i niemutowalność to nie są jakieś ideologiczne wymysły, tylko pomoc w utrzymaniu działającego kodu. Im mniej okazji na popełnienie błędu, tym mniejsze szanse, że ktoś go popełni.
Zmienianie kodu po to, aby przechodził testy jest brawurową oznaką braku umiejętności projektowych, który może prowadzić do problemów. Jak w przykładzie z posta, w którym jeden kod został zamieniony na zupełnie inny kod, który robi coś zupełnie innego.

Maciej Cąderek napisał(a):

Widać za dużo chcę na raz ;) Chyba nie będę się wygłupiał i zostawię tak jak jest (opcja 2), ale jak ktoś jeszcze ma jakieś wnioski to zapraszam - przyda się na przyszłość.

Nic nowego nie dopiszę - jeśli coś jest tak skomplikowane, że chcesz to przetestować, to trzeba to wydzielić.

1 użytkowników online, w tym zalogowanych: 0, gości: 1