Upgrade to Pro — share decks privately, control downloads, hide ads and more …

to-mock-or-not-to-mock.pdf

David Tanzer
September 21, 2022

 to-mock-or-not-to-mock.pdf

Are mocks evil? Should we embrace them? What are the trade-offs? What about other test doubles? Those are the topics of this talk...

David Tanzer

September 21, 2022
Tweet

More Decks by David Tanzer

Other Decks in Technology

Transcript

  1. Inside-Out, no mocks: Chicago School TDD Outside-in, embrace mocks: London

    School TDD Outside-in, no mocks: Munich School TDD @dtanzer 3
  2. 2. Powerful, Dangerous Tool 2. Powerful, Dangerous Tool 2. Powerful,

    Dangerous Tool 2. Powerful, Dangerous Tool 2. Powerful, Dangerous Tool
  3. { foo; bar=baz } it('returns option for correctly parsed option',

    () => { const option = 'foo = bar' const result = optionParser.parse(option, 0, option.length) expect(result).toHaveProperty('startIndex', 0) expect(result).toHaveProperty('length', option.length) expect(result?.content).toHaveProperty('key', 'foo') expect(result?.content).toHaveProperty('value', 'bar') expect(result?.content).toHaveProperty('asText', 'foo = bar') }) @dtanzer 9
  4. { foo; bar=baz } it('returns option for correctly parsed option',

    () => { const option = 'foo = bar' const result = optionParser.parse(option, 0, option.length) expect(result).toHaveProperty('startIndex', 0) expect(result).toHaveProperty('length', option.length) expect(result?.content).toHaveProperty('key', 'foo') expect(result?.content).toHaveProperty('value', 'bar') expect(result?.content).toHaveProperty('asText', 'foo = bar') }) @dtanzer 17
  5. Easy-Peasy! Easy-Peasy! Easy-Peasy! Easy-Peasy! Easy-Peasy! if(value) { return { startIndex:

    start, length: i, content: new UpdatableOption( text.substring(start, start+i), ident.foundText, value.foundText.trim(), start, i, this, ), } } @dtanzer 18
  6. { foo; bar=baz } it('parses key-value option at start of

    the options block', () => { const options = '{ foo = bar}' const result = parse(options) expect(result?.content.asMap).toHaveProperty('foo', 'bar') expect(result?.length).toEqual(options.length) }) @dtanzer 20
  7. But! But! But! But! But! let nextParser = this. let

    nextOption: ParserResult<Option> | null do { nextOption = start+i, length-i) if(nextOption) { // ... } } while(nextOption != null) defaultOptionParser nextParser.parse(text, @dtanzer 21
  8. 3. No Mocks: Error Boundary 3. No Mocks: Error Boundary

    3. No Mocks: Error Boundary 3. No Mocks: Error Boundary 3. No Mocks: Error Boundary
  9. Test Doubles Test Doubles Test Doubles Test Doubles Test Doubles

    Dummy: Just a value Stub: Returns canned answers Fake: Simpler implementation Mock: Has interaction-verification baked in Spy: Can be asked questions about interactions later Martin Fowler: Mocks aren't Stubs @dtanzer 23
  10. Dummy: Just Make it Run! Dummy: Just Make it Run!

    Dummy: Just Make it Run! Dummy: Just Make it Run! Dummy: Just Make it Run! @dtanzer 24
  11. it('shows a tab with a given name when named tab

    is registered',()=> { const { tabs, tabsModel, } = createApplicationTabs() act(() => {tabsModel.registerTab({id: 'dummy', name: 'tab name'})}) tabs.update() expect(tabs).to.have.descendants(Tab) expect(tabs.find(Tab).at(0)).to.have.text('tab name') }) @dtanzer 25
  12. const locations: [string, any][] = [ [ 'left', RightArrow, ],

    //... ] locations.forEach(td => { it(`creates a ${td[1]} when direction is ${td[0]}`, () => { const arrowRenderer = mount( <ArrowRenderer location={td[0]}> </ArrowRenderer> ) expect(arrowRenderer.find(td[1])).to.have.length(1) }) }) text @dtanzer 26
  13. const locations: [string, any][] = [ [ 'left', RightArrow, ],

    //... ] locations.forEach(td => { it(`creates a ${td[1]} when direction is ${td[0]}`, () => { const arrowRenderer = mount( <ArrowRenderer location={td[0]}> </ArrowRenderer> ) expect(arrowRenderer.find(td[1])).to.have.length(1) }) }) dummy @dtanzer 27
  14. 4. Make Test-Doubles Visible 4. Make Test-Doubles Visible 4. Make

    Test-Doubles Visible 4. Make Test-Doubles Visible 4. Make Test-Doubles Visible
  15. const createPresentationWindow = ( overrideProperties: Partial<PresentationWindowProperties> = {} ) =>

    { const defaultProperties: Required<PresentationWindowProperties>={ _getVersion: () => 'dummy', //... } const properties = { ...defaultProperties, ...overrideProperties, } const result = mount(<PresentationWindow {...properties} />) result.update() return result } @dtanzer 29
  16. Stub: Answer-in-a-Can Stub: Answer-in-a-Can Stub: Answer-in-a-Can Stub: Answer-in-a-Can Stub: Answer-in-a-Can

    it('displays the version number', () => { const presentationWindow = createPresentationWindow({ _getVersion: () => '1.2.3', }) expect(/*...*/).toHaveTextContent('1.2.3') }) @dtanzer 30
  17. Fake: Something, but not Real Fake: Something, but not Real

    Fake: Something, but not Real Fake: Something, but not Real Fake: Something, but not Real @dtanzer 32
  18. const createPresentationWindow = ( overrideProperties: Partial<PresentationWindowProperties> = {} ) =>

    { const defaultProperties: Required<PresentationWindowProperties>={ _setTimeout: (cb: ()=>unknown) => { cb() }, //... } const properties = { ...defaultProperties, ...overrideProperties, } const result = mount(<PresentationWindow {...properties} />) result.update() return result } @dtanzer 33
  19. Setting Presentation Data Setting Presentation Data Setting Presentation Data Setting

    Presentation Data Setting Presentation Data @dtanzer 35
  20. const onSetPresentationData = (callback: OnSetPresentationDataCB) => { const document =

    parseMarkdown('# first\n# second\n# third\n') callback(document, 'design', 1, 'basePath', new Annotations().serialize()) } const createPresentationWindow = ( overrideProperties: Partial<PresentationWindowProperties> = {} ) => { const defaultProperties: Required<PresentationWindowProperties>={ _onSetPresentationData: onSetPresentationData, //... } //... } @dtanzer 36
  21. 5. Fake DMZ / Outside World 5. Fake DMZ /

    Outside World 5. Fake DMZ / Outside World 5. Fake DMZ / Outside World 5. Fake DMZ / Outside World
  22. Spy: I Know what You Did to Me! Spy: I

    Know what You Did to Me! Spy: I Know what You Did to Me! Spy: I Know what You Did to Me! Spy: I Know what You Did to Me! @dtanzer 38
  23. Mock: Use Correctly or Blow Up Mock: Use Correctly or

    Blow Up Mock: Use Correctly or Blow Up Mock: Use Correctly or Blow Up Mock: Use Correctly or Blow Up @dtanzer 39
  24. it('sends start-video event when start button was clicked', () =>

    { const startVideoMock = mock<(id: string)=>void>('startVideoMock') const { controls } = startConfiguration({ _startVideo: instance(startVideoMock), }) act(() => { controls.find('.start-mirroring').at(0).simulate('click') }) controls.update() }) when(startVideoMock(anyString())).return().once() verify(startVideoMock) @dtanzer 40
  25. Result-Based Result-Based Result-Based Result-Based Result-Based Observe result or state Verify

    result matches the expected result Use dummies, stubs or fakes if you need them expect "Frontdoor testing" Interaction-Based Interaction-Based Interaction-Based Interaction-Based Interaction-Based Nothing to observe Verify expected interaction has happened Use mocks and/or spies; and dummies, stubs or fakes if you need them when / verify Backdoor testing @dtanzer 41
  26. 6. Result -or- Interaction 6. Result -or- Interaction 6. Result

    -or- Interaction 6. Result -or- Interaction 6. Result -or- Interaction
  27. Special Case: Jest Mock ES-Module Special Case: Jest Mock ES-Module

    Special Case: Jest Mock ES-Module Special Case: Jest Mock ES-Module Special Case: Jest Mock ES-Module "^(.*)/MonacoEditor$": "<rootDir>/__mocks__/reactMonacoMock.js", module.exports = { languages: { register: function(language) {}, setMonarchTokensProvider: function(name, tokens) {}, registerCompletionItemProvider: function(name, provider) {} }, editor: { defineTheme: function(name, theme) {} } }; @dtanzer 45
  28. Mock / Fake Close to DMZ Mock / Fake Close

    to DMZ Mock / Fake Close to DMZ Mock / Fake Close to DMZ Mock / Fake Close to DMZ it('loads 2 designs when 2 designs are found', () => { const findSlideDesignsMock = mock<(p: string, d: SlideDesign[])=>void>('findDesignsMock') when(findSlideDesignsMock(anyString(), anyArray())) .callFake((p, d) => { d.push({ /* ... */ }) d.push({ /* ... */ }) }) const designs = loadAllDesigns(undefined, instance(findSlideDesignsMock)) expect(loadedDesigns).to.have.length(2) expect(loadedDesigns[0]).to.contain({ id: 'design/1', }) expect(loadedDesigns[1]).to.contain({ id: 'design/2', }) }) @dtanzer 46
  29. it('clones the design when everything is OK', () => {

    const cloneDesignFolderMock = mock<( id: string, basePath: string | undefined, pathToClone: string ) => string | void>('cloneDesignMock') when(cloneDesignFolderMock( 'foo/bar', '/base/path', 'path/to/clone' )).return().once() cloneDesign('foo/bar', '/base/path', 'path/to/clone', () => [], instance(cloneDesignFolderMock)) verify(cloneDesignFolderMock) }) @dtanzer 47
  30. 7. Don't Mock what You don't Own 7. Don't Mock

    what You don't Own 7. Don't Mock what You don't Own 7. Don't Mock what You don't Own 7. Don't Mock what You don't Own
  31. it('finds existing designs', () => { const designs: SlideDesign[] =

    [] findSlideDesigns(path.join(__dirname, 'designs'), designs) expect(designs).to.have.length(2) }) |- findSlideDesigns.spec.ts |- designs | \- marmota | |- d1 | | |- design.json | | |- design.css | | \- preview.md | |- d2 ... @dtanzer 50
  32. 8. Integration-Test at Boundary 8. Integration-Test at Boundary 8. Integration-Test

    at Boundary 8. Integration-Test at Boundary 8. Integration-Test at Boundary
  33. Boundary AND Hard to Test Boundary AND Hard to Test

    Boundary AND Hard to Test Boundary AND Hard to Test Boundary AND Hard to Test Electron IPC Electron IPC Electron IPC Electron IPC Electron IPC sendSync(channel: C_MAIN, ...args: any[]) { return ipcRenderer.sendSync(channel, ...args) } sendSync(window: BrowserWindow, channel: C_RENDERER, ...args: any[]) { window.webContents.send(channel, ...args) } @dtanzer 52
  34. export type ChannelToMain = { '--annotation--delete-annotation': (fromWindow: AnnotationWindowName, annotationId: string)

    => unknown, } sendSync<C extends keyof C_MAIN>( channel: C, ...args: InferArguments<C_MAIN[C]> ): InferReturnType<C_MAIN[C]> { if(typeof channel !== 'string') throw new Error('...') return ipcRenderer.sendSync(channel, ...args) } @dtanzer 53
  35. export interface AnnotationsApi { deleteAnnotation: ChannelToMain['--annotation--delete-annotation'] } export const annotations:

    AnnotationsApi = { deleteAnnotation: (fromWindow: AnnotationWindowName, annotationId: string) => rendererIpc.sendSync( '--annotation--delete-annotation', fromWindow, annotationId ) } @dtanzer 54
  36. on(callbacks: { [key in keyof C_MAIN]: C_MAIN[key]}) { Object.keys(callbacks).forEach((channel) =>

    { if(typeof channel !== 'string') throw new Error('...') ipcMain.on(channel, (ipcEvent, ...args) => { ipcEvent.returnValue=callbacks[channel](...args, ipcEvent) }) }) } '--annotation--delete-annotation': (fromWindow: AnnotationWindowName, annotationId: string) => { // ... }, @dtanzer 55
  37. 9. Leverage the Type System 9. Leverage the Type System

    9. Leverage the Type System 9. Leverage the Type System 9. Leverage the Type System
  38. 1. Tradeoffs Everywhere 2. Powerful, Dangerous Tool 3. No Mocks:

    Error Boundary 4. Make Test-Doubles Visible 5. Fake DMZ / Outside World 6. Result- -or- Interaction-based 7. Don't Mock what You don't Own 8. Integration-Test at Boundary 9. Leverage the Type System @dtanzer 58
  39. Heuristics: Mocks Heuristics: Mocks Heuristics: Mocks Heuristics: Mocks Heuristics: Mocks

    Do not mock what you don't own Avoid mocks to make untestable code testable Avoid mocks that return mocks Avoid expect and verify in the same test Don't mix result-based and interaction-based testing @dtanzer 59
  40. About David About David About David About David About David

    Trainer, Coach, Developer https://davidtanzer.net @dtanzer
  41. Press [ESC] to exit presentation... Presented with Presented with Presented

    with Presented with Presented with marmota.app @dtanzer 61