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. View Slide

  2. 1. Tradeoffs Everywhere
    1. Tradeoffs Everywhere
    1. Tradeoffs Everywhere
    1. Tradeoffs Everywhere
    1. Tradeoffs Everywhere

    View Slide

  3. Inside-Out, no mocks: Chicago School TDD
    Outside-in, embrace mocks: London School TDD
    Outside-in, no mocks: Munich School TDD
    @dtanzer
    3

    View Slide

  4. To Mock or Not to Mock
    @dtanzer
    4

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. 2. Powerful, Dangerous Tool
    2. Powerful, Dangerous Tool
    2. Powerful, Dangerous Tool
    2. Powerful, Dangerous Tool
    2. Powerful, Dangerous Tool

    View Slide

  9. { 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

    View Slide

  10. Andrea Angealla: Universal Architecture (Coined by J.B. Rainsberger)
    @dtanzer
    10

    View Slide

  11. @dtanzer
    11

    View Slide

  12. @dtanzer
    12

    View Slide

  13. @dtanzer
    13

    View Slide

  14. @dtanzer
    14

    View Slide

  15. @dtanzer
    15

    View Slide

  16. @dtanzer
    16

    View Slide

  17. { 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

    View Slide

  18. 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

    View Slide

  19. @dtanzer
    19

    View Slide

  20. { 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

    View Slide

  21. But!
    But!
    But!
    But!
    But!
    let nextParser = this.

    let nextOption: ParserResult | null

    do {

    nextOption = start+i, length-i)

    if(nextOption) {

    // ...

    }

    } while(nextOption != null)
    defaultOptionParser
    nextParser.parse(text,
    @dtanzer
    21

    View Slide

  22. 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

    View Slide

  23. 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

    View Slide

  24. 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

    View Slide

  25. 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

    View Slide

  26. const locations: [string, any][] = [

    [ 'left', RightArrow, ],

    //...

    ]

    locations.forEach(td => {

    it(`creates a ${td[1]} when direction is ${td[0]}`, () => {

    const arrowRenderer = mount(


    )



    expect(arrowRenderer.find(td[1])).to.have.length(1)

    })

    })
    text
    @dtanzer
    26

    View Slide

  27. const locations: [string, any][] = [

    [ 'left', RightArrow, ],

    //...

    ]

    locations.forEach(td => {

    it(`creates a ${td[1]} when direction is ${td[0]}`, () => {

    const arrowRenderer = mount(


    )



    expect(arrowRenderer.find(td[1])).to.have.length(1)

    })

    })
    dummy
    @dtanzer
    27

    View Slide

  28. 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

    View Slide

  29. const createPresentationWindow = (

    overrideProperties: Partial = {}

    ) => {

    const defaultProperties: Required={

    _getVersion: () => 'dummy',

    //...

    }

    const properties = { ...defaultProperties, ...overrideProperties, }



    const result = mount(
    {...properties}

    />)

    result.update()

    return result

    }
    @dtanzer
    29

    View Slide

  30. 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

    View Slide

  31. @dtanzer
    31

    View Slide

  32. 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

    View Slide

  33. const createPresentationWindow = (

    overrideProperties: Partial = {}

    ) => {

    const defaultProperties: Required={

    _setTimeout: (cb: ()=>unknown) => { cb() },

    //...

    }

    const properties = { ...defaultProperties, ...overrideProperties, }



    const result = mount(
    {...properties}

    />)

    result.update()

    return result

    }
    @dtanzer
    33

    View Slide

  34. @dtanzer
    34

    View Slide

  35. Setting Presentation Data
    Setting Presentation Data
    Setting Presentation Data
    Setting Presentation Data
    Setting Presentation Data
    @dtanzer
    35

    View Slide

  36. 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 = {}

    ) => {

    const defaultProperties: Required={

    _onSetPresentationData: onSetPresentationData,

    //...

    }

    //...

    }
    @dtanzer
    36

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

  39. 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

    View Slide

  40. it('sends start-video event when start button was clicked', () => {

    const startVideoMock = mockvoid>('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

    View Slide

  41. 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

    View Slide

  42. 6. Result -or- Interaction
    6. Result -or- Interaction
    6. Result -or- Interaction
    6. Result -or- Interaction
    6. Result -or- Interaction

    View Slide

  43. @dtanzer
    43

    View Slide

  44. @dtanzer
    44

    View Slide

  45. 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$": "/__mocks__/reactMonacoMock.js",
    module.exports = {

    languages: {

    register: function(language) {},

    setMonarchTokensProvider: function(name, tokens) {},

    registerCompletionItemProvider: function(name, provider) {}

    },

    editor: {

    defineTheme: function(name, theme) {}

    }

    };
    @dtanzer
    45

    View Slide

  46. 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 =

    mockvoid>('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

    View Slide

  47. 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

    View Slide

  48. 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

    View Slide

  49. @dtanzer
    49

    View Slide

  50. 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

    View Slide

  51. 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

    View Slide

  52. 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

    View Slide

  53. export type ChannelToMain = {

    '--annotation--delete-annotation':

    (fromWindow: AnnotationWindowName, annotationId: string)

    => unknown,

    }
    sendSync(

    channel: C, ...args: InferArguments

    ): InferReturnType {

    if(typeof channel !== 'string') throw new Error('...')

    return ipcRenderer.sendSync(channel, ...args)

    }
    @dtanzer
    53

    View Slide

  54. 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

    View Slide

  55. 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

    View Slide

  56. 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

    View Slide

  57. To Recap...
    @dtanzer
    57

    View Slide

  58. 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

    View Slide

  59. 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

    View Slide

  60. About David
    About David
    About David
    About David
    About David
    Trainer, Coach, Developer
    https://davidtanzer.net
    @dtanzer

    View Slide

  61. Press [ESC] to exit presentation...
    Presented with
    Presented with
    Presented with
    Presented with
    Presented with
    marmota.app
    @dtanzer
    61

    View Slide