การทดสอบหน่วยของฟังก์ชันส่วนตัวด้วยมอคค่าและโหนด js


131

ฉันใช้มอคค่าเพื่อทดสอบหน่วยแอปพลิเคชันที่เขียนขึ้นสำหรับ node.js

ฉันสงสัยว่าเป็นไปได้ไหมที่จะใช้ฟังก์ชันทดสอบหน่วยที่ยังไม่ได้ส่งออกในโมดูล

ตัวอย่าง:

ฉันมีฟังก์ชันมากมายที่กำหนดไว้เช่นนี้ใน foobar.js

function private_foobar1(){
    ...
}

function private_foobar2(){
    ...
}

และฟังก์ชันบางอย่างที่ส่งออกเป็นสาธารณะ:

exports.public_foobar3 = function(){
    ...
}

กรณีทดสอบมีโครงสร้างดังนี้:

describe("private_foobar1", function() {
    it("should do stuff", function(done) {
        var stuff = foobar.private_foobar1(filter);
        should(stuff).be.ok;
        should(stuff).....

เห็นได้ชัดว่าสิ่งนี้ใช้ไม่ได้เนื่องจากprivate_foobar1ไม่มีการส่งออก

วิธีที่ถูกต้องในการทดสอบหน่วยส่วนตัวคืออะไร? มอคค่ามีวิธีการในตัวสำหรับการทำเช่นนั้นหรือไม่?


ที่เกี่ยวข้อง: stackoverflow.com/questions/14874208
dskrvk

คำตอบ:


64

หากโมดูลไม่ได้ส่งออกฟังก์ชันจะไม่สามารถเรียกใช้โดยใช้รหัสทดสอบภายนอกโมดูลได้ นั่นเป็นเพราะวิธีการทำงานของ JavaScript และ Mocha ไม่สามารถหลีกเลี่ยงสิ่งนี้ได้ด้วยตัวเอง

ในบางกรณีที่ฉันพิจารณาแล้วว่าการทดสอบฟังก์ชันส่วนตัวเป็นสิ่งที่ถูกต้องสิ่งที่ฉันทำคือตั้งค่าตัวแปรสภาพแวดล้อมบางอย่างที่โมดูลของฉันตรวจสอบเพื่อพิจารณาว่ากำลังทำงานในการตั้งค่าการทดสอบหรือไม่ หากทำงานในการตั้งค่าการทดสอบระบบจะส่งออกฟังก์ชันเพิ่มเติมที่ฉันสามารถเรียกใช้ระหว่างการทดสอบได้

คำว่า "สิ่งแวดล้อม" ถูกนำมาใช้อย่างหลวม ๆ ในที่นี้ อาจหมายถึงการตรวจสอบprocess.envหรืออย่างอื่นที่สามารถสื่อสารกับโมดูล "คุณกำลังทดสอบอยู่" อินสแตนซ์ที่ฉันต้องทำสิ่งนี้อยู่ในสภาพแวดล้อม RequireJS และฉันใช้module.configเพื่อจุดประสงค์นี้


2
ค่าที่ส่งออกตามเงื่อนไขดูเหมือนจะเข้ากันไม่ได้กับโมดูล ES6 ฉันได้รับSyntaxError: 'import' and 'export' may only appear at the top level
aij

1
@aij ใช่เนื่องจากการส่งออกคง ES6 คุณไม่สามารถใช้import, exportภายในของบล็อก ในที่สุดคุณจะสามารถทำสิ่งนี้ใน ES6 ให้สำเร็จด้วยตัวโหลดระบบ วิธีหนึ่งในการแก้ไขปัญหาตอนนี้คือการใช้module.exports = process.env.NODE_ENV === 'production' ? require('prod.js') : require('dev.js')และเก็บความแตกต่างของรหัส es6 ของคุณในไฟล์ที่เกี่ยวข้อง
cchamberlain

2
ฉันเดาว่าถ้าคุณมีพื้นที่ครอบคลุมทั้งหมดแสดงว่าคุณกำลังทดสอบฟังก์ชันส่วนตัวทั้งหมดของคุณไม่ว่าคุณจะเปิดเผยหรือไม่
Ziggy

1
@aij คุณสามารถส่งออกตามเงื่อนไข ... ดูคำตอบนี้: stackoverflow.com/questions/39583958/…
RayLoveless

187

ตรวจสอบโมดูลrewire ช่วยให้คุณได้รับ (และจัดการ) ตัวแปรและฟังก์ชันส่วนตัวภายในโมดูล

ดังนั้นในกรณีของคุณการใช้งานจะเป็นดังนี้:

var rewire = require('rewire'),
    foobar = rewire('./foobar'); // Bring your module in with rewire

describe("private_foobar1", function() {

    // Use the special '__get__' accessor to get your private function.
    var private_foobar1 = foobar.__get__('private_foobar1');

    it("should do stuff", function(done) {
        var stuff = private_foobar1(filter);
        should(stuff).be.ok;
        should(stuff).....

3
@Jaro รหัสส่วนใหญ่ของฉันอยู่ในรูปแบบของโมดูล AMD ซึ่ง rewire ไม่สามารถจัดการได้ (เนื่องจากโมดูล AMD เป็นฟังก์ชัน แต่ rewire ไม่สามารถจัดการ "ตัวแปรภายในฟังก์ชัน" ได้) หรือเกิดขึ้นสถานการณ์อื่นที่ rewire ไม่สามารถจัดการได้ จริงๆแล้วคนที่จะดู rewire ควรอ่านข้อ จำกัด ก่อน (ที่ลิงก์ไว้ก่อนหน้านี้) ก่อนที่จะลองใช้ ฉันไม่มีแอปเดียวที่ก) ต้องการส่งออกข้อมูล "ส่วนตัว" และ b) ไม่พบข้อ จำกัด ในการ rewire
Louis

1
เพียงจุดเล็ก ๆ ความครอบคลุมของรหัสอาจไม่สามารถรับการทดสอบที่เขียนเช่นนี้ได้ อย่างน้อยนั่นคือสิ่งที่ฉันเคยเห็นโดยใช้เครื่องมือครอบคลุมในตัวของ Jest
Mike Stead

Rewire เล่นได้ไม่ดีกับเครื่องมือล้อเลียนอัตโนมัติของ jest เช่นกัน ฉันยังคงมองหาวิธีที่จะใช้ประโยชน์จาก jest และเข้าถึงตัวแทนส่วนตัว
btburton42

ดังนั้นฉันจึงลองทำงานนี้ แต่ฉันใช้ typescript ซึ่งฉันเดาว่าเป็นสาเหตุของปัญหานี้ โดยทั่วไปฉันได้รับข้อผิดพลาดต่อไปนี้: Cannot find module '../../package' from 'node.js'. ใครที่คุ้นเคยกับเรื่องนี้?
clu

ReWire ทำงานได้ดีใน.ts, typescriptผมทำงานโดยใช้ts-node @clu
muthukumar Selvaraj

24

นี่คือขั้นตอนการทำงานที่ดีมากในการทดสอบวิธีการส่วนตัวของคุณซึ่งอธิบายโดย Philip Walton วิศวกรของ Google ในบล็อก

หลัก

  • เขียนโค้ดของคุณตามปกติ
  • ผูกวิธีการส่วนตัวของคุณกับวัตถุในกลุ่มรหัสที่แยกจากกันทำเครื่องหมายตาม_ตัวอย่าง
  • ล้อมรอบกลุ่มรหัสนั้นด้วยความคิดเห็นเริ่มต้นและสิ้นสุด

จากนั้นใช้งานการสร้างหรือระบบการสร้างของคุณเอง (สำหรับตัวอย่างรหัสคำราม) เพื่อดึงกลุ่มนี้ออกเพื่อสร้างงานสร้าง

การทดสอบของคุณสามารถเข้าถึง API ส่วนตัวของคุณได้

เศษเล็กเศษน้อย

เขียนรหัสของคุณดังนี้:

var myModule = (function() {

  function foo() {
    // private function `foo` inside closure
    return "foo"
  }

  var api = {
    bar: function() {
      // public function `bar` returned from closure
      return "bar"
    }
  }

  /* test-code */
  api._foo = foo
  /* end-test-code */

  return api
}())

และงานที่หนักอึ้งของคุณเช่นนั้น

grunt.registerTask("test", [
  "concat",
  "jshint",
  "jasmine"
])
grunt.registerTask("deploy", [
  "concat",
  "strip-code",
  "jshint",
  "uglify"
])

ลึกมากขึ้น

ในบทความต่อมาจะอธิบายถึง "ทำไม" ของ "การทดสอบวิธีส่วนตัว"


1
นอกจากนี้ยังพบปลั๊กอิน webkit ที่ดูเหมือนว่าสามารถรองรับเวิร์กโฟลว์ที่คล้ายกัน: webpack-strip-block
JRulle

21

หากคุณต้องการให้มันง่ายเพียงแค่ส่งออกสมาชิกส่วนตัวเช่นกัน แต่แยกออกจาก API สาธารณะอย่างชัดเจนด้วยหลักการบางอย่างเช่นนำหน้าด้วย _หรือซ้อนไว้ภายใต้วัตถุส่วนตัวเดียว

var privateWorker = function() {
    return 1
}

var doSomething = function() {
    return privateWorker()
}

module.exports = {
    doSomething: doSomething,
    _privateWorker: privateWorker
}

7
ฉันได้ทำสิ่งนี้ในกรณีที่โมดูลทั้งหมดตั้งใจให้เป็นแบบส่วนตัวไม่ใช่เพื่อการบริโภคทั่วไป แต่สำหรับโมดูลวัตถุประสงค์ทั่วไปฉันชอบที่จะเปิดเผยสิ่งที่ฉันต้องการสำหรับการทดสอบเท่านั้นเมื่อรหัสจะถูกทดสอบ เป็นความจริงที่ว่าท้ายที่สุดแล้วไม่มีอะไรที่จะป้องกันไม่ให้ใครบางคนเข้าถึงของส่วนตัวได้โดยการปลอมสภาพแวดล้อมการทดสอบ แต่เมื่อมีการแก้ไขข้อบกพร่องในแอปพลิเคชันของตนเองฉันอยากให้พวกเขาไม่เห็นสัญลักษณ์ที่ไม่จำเป็นต้องเป็น เป็นส่วนหนึ่งของ API สาธารณะ วิธีนี้จะไม่มีการล่อลวงในทันทีที่จะละเมิด API สำหรับวัตถุประสงค์ที่ไม่ได้ออกแบบมาสำหรับ
Louis

2
คุณยังสามารถใช้ไวยากรณ์ที่ซ้อนกัน {... private : {worker: worker}}
Jason

2
หากโมดูลเป็นฟังก์ชันที่แท้จริงทั้งหมดฉันก็ไม่เห็นข้อเสียในการทำเช่นนี้ หากคุณกำลังรักษาและสถานะการกลายพันธุ์ระวัง ...
Ziggy

5

ฉันสร้างแพ็คเกจ npm เพื่อจุดประสงค์นี้ซึ่งคุณอาจพบว่ามีประโยชน์: ต้องใช้จาก

โดยทั่วไปคุณเปิดเผยวิธีการที่ไม่ใช่สาธารณะโดย:

module.testExports = {
    private_foobar1: private_foobar1,
    private_foobar2: private_foobar2,
    ...
}

หมายเหตุ: testExportsสามารถเป็นชื่อที่ถูกต้องที่คุณต้องการยกเว้นexportsแน่นอน

และจากโมดูลอื่น:

var requireFrom = require('require-from');
var private_foobar1 = requireFrom('testExports', './path-to-module').private_foobar1;

1
ฉันไม่เห็นประโยชน์ในทางปฏิบัติสำหรับวิธีนี้ ไม่ทำให้สัญลักษณ์ "ส่วนตัว" เป็นส่วนตัวมากขึ้น (ทุกคนสามารถเรียกrequireFromกับพารามิเตอร์ที่เหมาะสม.) นอกจากนี้ถ้าโมดูลที่มีtextExportsการโหลดโดยrequireโทรก่อนที่จะ requireFromโหลดมันจะกลับมาrequireFrom undefined(ฉันเพิ่งทดสอบ) แม้ว่าจะสามารถควบคุมลำดับการโหลดของโมดูลได้บ่อยครั้ง แต่ก็ไม่สามารถใช้งานได้จริงเสมอไป (ตามหลักฐานจากคำถามของ Mocha เกี่ยวกับ SO) โดยทั่วไปแล้วโซลูชันนี้จะใช้ไม่ได้กับโมดูลประเภท AMD (ฉันโหลดโมดูล AMD ในโหนดทุกวันเพื่อทำการทดสอบ)
หลุยส์

ไม่ควรใช้กับโมดูล AMD! Node.js ใช้ common.js และหากคุณเปลี่ยนไปใช้ AMD แสดงว่าคุณกำลังทำผิดปกติ
jemiloii

@JemiloII นักพัฒนาหลายร้อยคนใช้ Node.js ทุกวันเพื่อทดสอบโมดูล AMD ไม่มีอะไร "ผิดปกติ" ในการทำเช่นนั้น สิ่งที่คุณสามารถพูดได้มากที่สุดก็คือ Node.js ไม่ได้มาพร้อมกับตัวโหลด AMD แต่ไม่ได้พูดอะไรมากนักเนื่องจาก Node มีตะขอที่ชัดเจนเพื่อขยายตัวโหลดเพื่อโหลดสิ่งที่นักพัฒนารูปแบบต้องการพัฒนา
Louis

มันอยู่นอกบรรทัดฐาน หากคุณต้องรวม amd loader ด้วยตนเองไม่ใช่บรรทัดฐานสำหรับ node.js ฉันไม่ค่อยเห็น AMD สำหรับโค้ด node.js ฉันจะเห็นมันสำหรับเบราว์เซอร์ แต่โหนด ไม่ฉันไม่ได้บอกว่ามันไม่ได้ทำเพียงแค่คำถามและคำตอบนี้ที่เรากำลังแสดงความคิดเห็นไม่ต้องพูดอะไรเกี่ยวกับโมดูล amd ดังนั้นหากไม่มีใครระบุว่าพวกเขาใช้ amd loader การส่งออกโหนดจึงไม่ควรทำงานกับ amd แม้ว่าฉันต้องการทราบว่า commonjs อาจกำลังจะหมดไปกับการส่งออก es6 ฉันหวังว่าวันหนึ่งเราทุกคนจะสามารถใช้วิธีการส่งออกเพียงวิธีเดียว
jemiloii

4

ฉันได้เพิ่มฟังก์ชันพิเศษที่ฉันชื่อInternal ()และส่งคืนฟังก์ชันส่วนตัวทั้งหมดจากที่นั่น นี้ภายใน ()ฟังก์ชั่นการส่งออกแล้ว ตัวอย่าง:

function Internal () {
  return { Private_Function1, Private_Function2, Private_Function2}
}

// Exports --------------------------
module.exports = { PublicFunction1, PublicFunction2, Internal }

คุณสามารถเรียกใช้ฟังก์ชันภายในดังนี้:

let test = require('.....')
test.Internal().Private_Function1()

ฉันชอบวิธีนี้มากที่สุดเพราะ:

  • เพียงฟังก์ชันเดียวเท่านั้นภายใน ()จะถูกส่งออกเสมอ นี้ภายใน ()ฟังก์ชั่นนั้นจะใช้ในการทดสอบฟังก์ชั่นส่วนตัว
  • ใช้งานง่าย
  • ผลกระทบต่ำต่อรหัสการผลิต (มีฟังก์ชันพิเศษเพียงฟังก์ชันเดียว)

2

ฉันติดตามคำตอบของ@barwinและตรวจสอบวิธีการทดสอบหน่วยด้วยrewireโมดูลฉันสามารถยืนยันได้ว่าโซลูชันนี้ใช้งานได้จริง

โมดูลควรจะต้องแบ่งเป็นสองส่วน - ส่วนหนึ่งสาธารณะและส่วนส่วนตัว สำหรับฟังก์ชันสาธารณะคุณสามารถทำได้ในรูปแบบมาตรฐาน:

const { public_foobar3 } = require('./foobar');

สำหรับขอบเขตส่วนตัว:

const privateFoobar = require('rewire')('./foobar');
const private_foobar1 = privateFoobar .__get__('private_foobar1');
const private_foobar2 = privateFoobar .__get__('private_foobar2');

เพื่อทราบข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ฉันได้สร้างตัวอย่างการทำงานที่มีการทดสอบโมดูลเต็มรูปแบบการทดสอบรวมถึงขอบเขตส่วนตัวและสาธารณะ

สำหรับข้อมูลเพิ่มเติมฉันขอแนะนำให้คุณตรวจสอบบทความ ( https://medium.com/@macsikora/how-to-test-private-functions-of-es6-module-fb8c1345b25f ) ที่อธิบายเรื่องนี้อย่างครบถ้วนซึ่งจะมีตัวอย่างโค้ดด้วย


2

ฉันรู้ว่านี่ไม่ใช่คำตอบที่คุณกำลังมองหา แต่สิ่งที่ฉันพบคือส่วนใหญ่แล้วถ้าฟังก์ชันส่วนตัวคุ้มค่ากับการทดสอบมันก็คุ้มค่าที่จะอยู่ในไฟล์ของตัวเอง

เช่นแทนที่จะมีเมธอดส่วนตัวในไฟล์เดียวกับแบบสาธารณะเช่นนี้ ...

src / สิ่ง / PublicInterface.js


function helper1 (x) {
    return 2 * x;
}

function helper2 (x) {
    return 3 * x;
}

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

... คุณแยกออกเป็นดังนี้:

src / สิ่ง / PublicInterface.js

import {helper1} from './internal/helper1.js';
import {helper2} from './internal/helper2.js';

export function publicMethod1(x) {
    return helper1(x);
}

export function publicMethod2(x) {
    return helper1(x) + helper2(x);
}

src / สิ่งภายใน / / helper1.js

export function helper1 (x) {
    return 2 * x;
}

src / สิ่งภายใน / / helper2.js

export function helper2 (x) {
    return 3 * x;
}

ด้วยวิธีนี้คุณสามารถทดสอบhelper1และhelper2ตามที่เป็นอยู่ได้อย่างง่ายดายโดยไม่ต้องใช้ Rewire และ "เวทมนตร์" อื่น ๆ (ซึ่งฉันพบว่ามีจุดเจ็บปวดของตัวเองในขณะที่แก้ไขจุดบกพร่องหรือเมื่อคุณพยายามที่จะก้าวไปสู่ ​​TypeScript ไม่ต้องพูดถึงคนที่ด้อยกว่า ความเข้าใจสำหรับเพื่อนร่วมงานใหม่) และพวกเขาอยู่ในโฟลเดอร์ย่อยที่เรียกว่าinternalหรืออะไรทำนองนั้นจะช่วยหลีกเลี่ยงการใช้งานโดยไม่ได้ตั้งใจในสถานที่ที่ไม่ได้ตั้งใจ


PS: อีกปัญหาที่พบด้วยวิธีการ "เอกชน" คือว่าถ้าคุณต้องการที่จะทดสอบpublicMethod1และpublicMethod2และเยาะเย้ยผู้ช่วยเหลืออีกครั้งคุณจำเป็นบางอย่างเช่น rewire จะทำเช่นนั้นได้ตามปกติ อย่างไรก็ตามหากไฟล์เหล่านี้อยู่ในไฟล์แยกกันคุณสามารถใช้Proxyquireเพื่อทำสิ่งนี้ได้ซึ่งไม่เหมือนกับ Rewire ที่ไม่ต้องการการเปลี่ยนแปลงใด ๆ ในกระบวนการสร้างของคุณอ่านและแก้ไขจุดบกพร่องได้ง่ายและทำงานได้ดีแม้กับ TypeScript


1

เพื่อให้มีวิธีการส่วนตัวสำหรับการทดสอบฉันทำสิ่งนี้:

const _myPrivateMethod: () => {};

const methods = {
    myPublicMethod1: () => {},
    myPublicMethod2: () => {},
}

if (process.env.NODE_ENV === 'test') {
    methods._myPrivateMethod = _myPrivateMethod;
}

module.exports = methods;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.