Skip to main content

What is Spec?

Spec is a specification of behavior that we record and replay. It is identified by the specName, which you defined when using the mocktomata:

mockto('<specName>', ...)

komondor('<specName>')

scenario('<specName>') // zucchini

Uniqueness of specName

Because specName is the identifier of Spec, the specName must be unique for each test in the same file.

For example, if you use "get friends" as the specName in two test of a test file, one of them will fail. This holds true even if you use some grouping mechanism such as describe() to put tests in different group.

However, it will be fine if the same specName is used in two different test files.

So what you can do is either making sure you use a different specName in the same file across describe() groups, or you can separate each group into different files. For example:

- someCode.group-1.spec.ts
- someCode.group-2.spec.ts

This is also the way tap family of test runners (e.g. ava) organize the tests.

What can be a spec subject?

spec subject is the code you want to record the behavior from. The most common use case is a gateway function or library, such as axios or fetch.

But it can be any other things, such as gateway to your database, file system, another thread, or even libraries or code from another team that might not be stable.

In essence, it can be anything that is external, suspect to chanages, or anything unstable.

SpecRecord

SpecRecord is the record of a Spec that we saved. It is saved in the .mocktomata folder under the root of your project. In the future, they can also be saved remotely too.

The done() method returns the SpecRecord, so you can inspect it if you wanted to.

spec()

spec() function is the function to specify the spec subject.

function spec(subject, options?): Promise<subject>

Where the return promise contains the "speced" subject which you can use in place of subject.

You can specify a mock using the options:

spec(subject, { mock: mockSubject })

When running in mock mode (e.g. mockto.mock(...)), the mockSubject will be used instead of the subject. In other modes, the mockSubject will be ignored.

This is useful to write a test with a mock subject, while the actual subject/remote system is not ready yet.

Spec.Options

Spec.Options is used by the mocktomata to configure the spec. It contains 3 properties:

  • timeout: How long will the spec wait before consider the subject failed to return. (default to 3000 ms)
  • logLevel: Log level for logging the behavior (default to logLevels.info)
  • emitLog: When true, emit logs to console (default false)

Here is how do specify the options for other mocktomata:

mockto(specName, options?, handler)
komondor(specName, options?)
scenario(specName, options?) // zucchini
incubator(specName, options?, handler)

reporter

reporter is a MemoryLogReporter from standard-log. It contains the logs generated by mocktomata so that you can inspect them.

A common use case is to inspect the logs make sure it does not contain any sensitive information.

SpecMode

Each Spec runs in a specific mode depends on the condition. There are 4 different SpecMode:

  • auto: the default mode which will resolve to either save or simulate mode.
  • save: actual calls will be made and the behavior are saved (i.e. recording).
  • simulate: actual calls are not made and the behavior are simulated (i.e. replay).
  • live: actual calls are made but the behavior are not saved.

done()

done() should be called at the end of each Spec. It is how the Spec knows it should save the SpecRecord or make sure the simulation are complete fully.

It is an async function returning the SpecRecord (done(): Promise<SpecRecord>).

You can inspec the SpecRecord if you want to, but typically you will just await or return it:


it('...', async () => {
// ...

await done()

// or return, if your test-runner support that.
return done()
}

cleanup()

cleanup() function is used to clean up the test environment.

You should call it at afterAll() or similar function:

afterall(mockto.cleanup)
afterall(komondor.cleanup)
afterall(scenario.cleanup) // zucchini
afterall(incubator.cleanup)

What happens is when the done() is not called, either you forget to do so or the test fails, the Spec does not know that and keep waiting for the done() call.

In that case you will see a warning like this (in jest):

A worker process has failed to exit gracefully and has been force exited.
This is likely caused by tests leaking due to improper teardown.
Try running with --detectOpenHandles to find leaks.
Active timers can also cause this, ensure that `.unref()` was called on them.

calling cleanup() tells mocktomata that all Spec are done so it can clean things up properly.

maskValue()

When interacting with external system, the calls may contain sensitive information such as username, password, auth token, etc.

Since mocktomata will save the SpecRecord, it is important to make sure these sensitive information will not be saved in your repository. The same goes for logs, as you may send the logs else where or save it in some files.

maskValue() allows you to indicate what values are considered sensitive, and mask them in the logs and in SpecRecord.

It support string and RegExp, and its signature is:

function maskValue(value: string | RegExp, replaceWith?: string)

The RegExp will be .exec() against the values, so if the sensitive information can occurs multiple times in the same string, use the /g global flag to make sure all occurrences are masked:

maskValue(/some-secret/g)
maskValue(/Bearer \.*/g)

The replaceWith defaults to "[masked]". You can change that if you want, and you can change it different values for each maskValue() call if you find that beneficial.

You must call maskValue() before calling spec(), because calling spec() will starting emitting/saving logs.

Depends on how the sensitive information is created, who created it, and what SpecMode the Spec is running in, the value flow through your code could be the actual sensitive value, or the masked value.

so in your test, you should avoid checking the value explicitly. Your code will probably work as-is. If you absolutely need to, you can use the replaceWith to set the masked value to something your code understand, and yet the value is safe to be saved in your source control.

ignoreMismatch()

Sometimes the input you provide to the test may not be static. For example, random number, date time, or hostnames.

In 7.0, reference values (string, object, array, function) are not validate for changes, i.e. even if you pass in a different string, the system will not fail it with ActionMismatch error. This is because we track the behavior, not and exact call, whenever possible.

This is a new concept and is subject to change if it does not work well.

On the other hand, primitive values such as number and boolean are validated.

If you want to tell spec() that it should not care about the changes, you can use ignoreMismatch() to ignore that specific value.

The value is compared using strict equivalence. (i.e. ===). For example:

test('...', async () => {
const date = new Date()

const spec = komondor('...')
spec.ignoreMismatch(date)

// ...
})