testdouble.js

repository·main·Indexed 23 days ago

https://github.com/testdouble/testdouble.js

An opinionated mocking library for JavaScript designed for TDD. It provides tools to create test doubles for functions, objects, and constructors using td.function(), td.object(), and td.constructor(). The library supports replacing dependencies in Node.js and ESM environments via td.replace() and td.replaceEsm(), stubbing responses with td.when(), and verifying interactions with td.verify(). It includes built-in support for TypeScript interfaces, function types, and classes, and provides a debugging API via td.explain().

Tokens
15.9K
Snippets
28
Records
96
Agent score
80%

What's inside testdouble.js

  1. Stubbing behavior with td.when()

    main

    Use td.when() to define how a test double should behave when it is called. This allows you to stub specific return values, exceptions, or side effects based on the arguments passed to the double.

    Key stubbing patterns include:

    • Argument matching: You can stub specific arguments, no arguments, or use matchers to loosen constraints.
    • Sequential returns: Stubbing multiple consecutive calls to return different values.
    • Callbacks: Stubbing APIs that rely on callback functions.
    • Exceptions: Using thenThrow() to simulate errors.
    • Promises: Using thenResolve() or thenReject() to simulate asynchronous behavior.
    • Side effects: Using thenDo() to execute custom logic when the stub is called.
  2. Avoid redundant verification of stubbed interactions

    main

    Do not call td.verify() for an interaction that has already been stubbed using td.when().

    If you use td.when() to provide a specific return value required for the code under test to function, the fact that the code successfully executed with that return value implicitly proves the interaction occurred. Adding td.verify() for the same interaction is redundant and creates brittle tests that are overly coupled to the implementation details.

    If you attempt to verify a stubbed invocation, testdouble.js will print a console warning.

  3. Capture arguments with td.matchers.captor()

    main

    An argument captor is a special matcher used to retrieve references to arguments passed to a test double, which is particularly useful for testing anonymous or privately-scoped callback functions.

    When you call captor.capture() inside a td.verify() call, the captor records the argument passed to it. You can then access this value via captor.value (for a single invocation) or captor.values (an array containing arguments from every invocation) to perform further assertions.

    This pattern allows you to test the logic inside a callback synchronously without needing to write asynchronous tests.

    var logger = td.function('logger'),
        fetcher = td.function('fetcher'),
        captor = td.matchers.captor()
    
    // Subject under test
    function logInvalidComments(fetcher, logger) {
      fetcher('/comments', function(response){
        response.comments.forEach(function(comment) {
          if(!comment.valid) {
            logger('Hey, '+comment.text+' is invalid')
          }
        })
      })
    }
    
    logInvalidComments(fetcher, logger)
    
    // 1. Capture the callback passed to fetcher
    td.verify(fetcher('/comments', captor.capture()))
    
    // 2. Manually invoke the captured callback with a mock response
    var response = {comments: [{valid: true}, {valid: false, text: 'PANTS'}]}
    captor.value(response)
    
    // 3. Verify the interaction with the logger
    td.verify(logger('Hey, PANTS is invalid'))
  4. When to use testdouble.js

    main

    testdouble.js is designed primarily for unit tests where you need to specify the collaboration between functions and objects. It is intended to help you test "collaborator" functions—those that depend on and invoke other functions to return values or trigger side effects.

    1. Pure Logic Functions: Functions that return a value based solely on arguments or state (e.g., add(5, 3)). Do not use test doubles here; simply provide the necessary arguments and verify the result.
    2. Collaborator Functions: Functions that orchestrate logic by calling other dependencies. This is the primary use case for testdouble.js. You can replace these dependencies with test doubles to stub responses or verify interactions.
    3. Mixed Abstraction Functions: Functions that contain both complex logic and dependency interactions. While test doubles can be used, they often make tests confusing. These functions are often a sign of poor design and should ideally be refactored into smaller, cleaner collaborator functions.

    Integration Tests

    We recommend against using testdouble.js in integrated test suites.

    Using test doubles in integration tests increases coupling between the test and the implementation, which reduces refactor safety and can lead to false negatives. For integration tests, it is better to use tools that operate at the boundary of your system (e.g., a fake HTTP server for network requests) rather than replacing internal functions.

    If you must use test doubles in integration tests, wrap 3rd-party dependencies in adapter functions and fake the adapters instead of the 3rd-party API directly. This prevents test doubles from leaking throughout your suite and allows you to improve API design via the adapters.

  5. Stub behavior with td.when()

    main

    In testdouble.js, stubbing is the process of configuring a test double to return a specific response for a given set of inputs. You achieve this using the td.when() function.

    To use td.when(), you must "rehearse" the call you want to stub by invoking the test double inside the td.when() arguments. This tells the library which specific invocation (and which arguments) the subsequent response should apply to.

    td.when() returns a configuration object that provides several response methods:

    • thenReturn(value): Returns a specific value.
    • thenThrow(error): Throws an exception.
    • thenDo(fn): Executes a side effect (a function).
    • thenResolve(value): Resolves a Promise with a value.
    • thenReject(reason): Rejects a Promise with a reason.
    var quack = td.function('quack')
    
    td.when(quack()).thenReturn('some return value')
    
    quack() // 'some return value'
  6. How custom argument matchers work

    main

    In testdouble.js, an argument matcher is any object passed into a td.when() or td.verify() invocation that possesses a __matches function. This function must return a truthy value when the argument matches and a falsy value when it does not.

    If an argument does not have a __matches property, testdouble.js defaults to using lodash's _.isEqual for a deep equality check. While you can implement the __matches interface manually, it is recommended to use td.matchers.create() to ensure better error reporting in td.explain calls and td.verify failures.

  7. Loosen stubbing requirements with argument matchers

    main

    By default, td.when() requires exact argument matches. To allow for more flexible testing, use argument matchers from td.matchers to satisfy stubbing requirements based on types, patterns, or logic.

    Available Matchers:

    • td.matchers.anything(): Ignores the parameter entirely. The stub matches regardless of what is passed for that argument.
    • td.matchers.isA(Type): Matches if the actual argument is an instance of the specified type (e.g., Number, String, or a custom constructor).
    • td.matchers.contains(pattern): Matches if the argument contains the specified portion. Supports:
      • Strings: Matches substrings or Regular Expressions.
      • Arrays: Matches if the array contains the specified elements.
      • Objects: Matches if the object contains the specified properties (supports deep/sparse property searches).
    • td.matchers.argThat(predicate): Matches if a truth-test function returns true for the argument.
    • td.matchers.not(value): Used with td.verify() to ensure a function was not called with a specific value.
    // Example: isA
    var eatBiscuit = td.function()
    td.when(eatBiscuit(td.matchers.isA(Number))).thenReturn('yum')
    eatBiscuit(5) // 'yum'
    
    // Example: contains (Object)
    td.when(brew(td.matchers.contains({container: {size: 'S'}}))).thenReturn('small coffee')
    brew({ingredient: 'beans', container: { type: 'cup', size: 'S'}}) // 'small coffee'
    
    // Example: argThat
    td.when(pet(td.matchers.argThat(function(animals){ return animals.length > 2 }))).thenReturn('goood')
    pet(['cat', 'dog', 'horse']) // 'goood'
  8. Verifying interactions with td.verify()

    main

    Use td.verify() to assert that a test double was invoked exactly as expected. This is primarily used when a dependency is being called for its side effects rather than for its return value.

    Best Practices:

    • Avoid redundant verifications: Never verify an invocation that was also stubbed. If the stubbing is required for the test to pass, adding a verification for that same call is redundant.
    • Verify only when necessary: Only use verification when there is no other way to assert that the subject under test is performing the correct action (e.g., when the function's purpose is a side effect).
    • Prefer pure functions: If a function returns a meaningful value, test the return value instead of verifying the invocation.
  9. Install testdouble.js

    main

    testdouble.js can be installed for different environments. The documentation provides specific guides for:

    • Node.js: Installation and setup for Node.js or Browserify environments.
    • Browsers: Installation for direct browser usage.
    • Initial Configuration: Setting up testdouble.js within your existing test suite.

    Refer to the installation guide for specific commands and configuration steps.

  10. Set up the `td` global shorthand

    main

    For convenience, it is recommended to require the library in a test helper and assign it to the global td object. This allows you to use the td shorthand throughout your test files.

    ES Modules

    import * as td from 'testdouble'

    CommonJS (Node.js)

    globalThis.td = require('testdouble')

    Browser

    The browser distribution sets window.td automatically.

    Note: You may need to configure your linter (like ESLint or Standard) to recognize td as a global variable to avoid 'variable is not defined' errors.

    // ES import syntax
    import * as td from 'testdouble'
    
    // CommonJS modules (e.g. Node.js)
    globalThis.td = require('testdouble')
    
    // Global set in our browser distribution
    window.td