> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fetchski-attract.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Japa tests that fake the site in one line, then assert what your app called.

`fakeAttract()` puts a fake site in the container for one test: it sends nothing, every call answers
a valid model, and you assert what was called. Every controller, service and job that injects
`SiteClient` (or `SiteClients`) gets the fake, and the real client is back when the test ends.

```bash Terminal theme={"dark"}
node ace make:test blog_posts --suite=functional
node ace test
```

## Through a route

```ts tests/functional/blog_posts.spec.ts theme={"dark"}
import { test } from '@japa/runner'
import { factories, fakeAttract, notFound } from '@fetchski/attract-cms-adonis/testing'

test.group('Blog posts', () => {
  test('lists the posts', async ({ client }) => {
    const site = fakeAttract({ 'blogPosts.listBlogs': factories.BlogList({ pages: 1 }) })

    const response = await client.get('/blog')

    response.assertStatus(200)
    site.assertCalled('blogPosts.listBlogs', (args) => args.page === 1)
  })

  test('a missing post is a 404', async ({ client }) => {
    fakeAttract({ 'blogPosts.getBlog': notFound() })

    const response = await client.get('/blog/gone')

    response.assertStatus(404)
  })
})
```

> [!TIP]
>
> Nothing is sent and no key is read: the placeholder in `.env.test` ([Configuration](https://docs.fetchski-attract.com/adonisjs/configuration.md#give-tests-a-placeholder))
> only satisfies `start/env.ts`. `fakeAttract()` answers the fake itself, with its assertions.

## A service, directly

```ts tests/unit/blog_service.spec.ts theme={"dark"}
import { test } from '@japa/runner'
import app from '@adonisjs/core/services/app'
import cache from '@adonisjs/cache/services/main'
import { factories, fakeAttract } from '@fetchski/attract-cms-adonis/testing'
import { BlogService } from '#services/blog_service'

test.group('BlogService', (group) => {
  group.each.setup(() => cache.clear())

  test('reads the site once, then the cache', async ({ assert }) => {
    const site = fakeAttract({
      'blogPosts.listBlogs': factories.BlogList({ results: [{ title: 'Hello' }] }),
    })
    const blog = await app.container.make(BlogService)

    const posts = await blog.latest()
    assert.equal(posts[0]?.title, 'Hello')
    await blog.latest()

    site.assertCalledTimes('blogPosts.listBlogs', 1)
  })
})
```

`app.container.make()` builds the service with the fake injected. `BlogService` caches the
site's answer ([Jobs, cache and health](https://docs.fetchski-attract.com/adonisjs/background-work.md#cache-the-answers)), so each test
starts from an empty cache.

## Answers and refusals

Give a method its answer by key, `<accessor>.<method>`. `fakeAttract()` takes the SDK's answers, all
on [TypeScript → Testing](https://docs.fetchski-attract.com/typescript/testing.md#answers), and the package hands you every helper:

```ts A refusal theme={"dark"}
import { fakeAttract, validationError } from '@fetchski/attract-cms-adonis/testing'

const site = fakeAttract({ 'testimonials.createTestimonial': validationError({ quote: 'Required.' }) })
```

```ts A site failure theme={"dark"}
import { fakeAttract, siteError } from '@fetchski/attract-cms-adonis/testing'

const site = fakeAttract({ 'blogPosts.listBlogs': siteError(500) })
// with the exception handler from Calling the API, the route answers 502
```

```ts Pages in turn theme={"dark"}
import { factories, fakeAttract, sequence } from '@fetchski/attract-cms-adonis/testing'

const site = fakeAttract({
  'blogPosts.listBlogs': sequence(
    factories.BlogList({ pages: 2 }),
    factories.BlogList({ pages: 2, currentPage: 2 })
  ),
})
```

The assertions (`assertCalled`, `assertCalledTimes`, `assertNotCalled`, `assertNothingCalled`) throw
`node:assert`'s `AssertionError`, which Japa reports like its own.

## Your own validation

A request your validator refuses never reaches the site:

```ts tests/functional/testimonials.spec.ts theme={"dark"}
import { test } from '@japa/runner'
import { factories, fakeAttract } from '@fetchski/attract-cms-adonis/testing'

test.group('Testimonials', () => {
  test('a testimonial is saved as a draft', async ({ client }) => {
    const site = fakeAttract({
      'testimonials.createTestimonial': factories.Testimonial({ id: 'new-id' }),
    })

    const response = await client
      .post('/testimonials')
      .json({ authorName: 'Jo', quote: 'Great service, thank you.', rating: 5 })

    response.assertStatus(201)
    response.assertBody({ id: 'new-id' })
    site.assertCalled(
      'testimonials.createTestimonial',
      (args) => args.aposMode === 'draft' && args.rating === 5
    )
  })

  test('a short quote never reaches the site', async ({ client }) => {
    const site = fakeAttract()

    const response = await client
      .post('/testimonials')
      .json({ authorName: 'Jo', quote: 'Great', rating: 5 })

    response.assertStatus(422)
    site.assertNothingCalled()
  })
})
```

## Many sites

`fakeAttractSites()` fakes every site and answers the directory: `.site('globex')` is that site's
fake, to assert on. The `SitesReport` service is on [Configuration](https://docs.fetchski-attract.com/adonisjs/configuration.md#many-sites-by-slug):

```ts tests/unit/sites_report.spec.ts theme={"dark"}
import { test } from '@japa/runner'
import app from '@adonisjs/core/services/app'
import { factories, fakeAttractSites } from '@fetchski/attract-cms-adonis/testing'
import { SitesReport } from '#services/sites_report'

test('reads each site', async ({ assert }) => {
  const sites = fakeAttractSites({
    'blogPosts.listBlogs': factories.BlogList({ results: [{ title: 'Hello' }] }),
  })
  const report = await app.container.make(SitesReport)

  const titles = await report.latestTitles(['acme', 'globex'])

  assert.deepEqual(titles, { acme: 'Hello', globex: 'Hello' })
  sites.site('globex').assertCalled('blogPosts.listBlogs')
})
```

## An Ace command of your own

Ace runs a command inside the test, and the fake reaches it like any other class. A command that
prints the site's newest posts:

```ts tests/unit/latest_posts.spec.ts theme={"dark"}
import { test } from '@japa/runner'
import ace from '@adonisjs/core/services/ace'
import { factories, fakeAttract } from '@fetchski/attract-cms-adonis/testing'
import LatestPosts from '../../commands/latest_posts.js'

test('prints the newest posts', async ({ cleanup }) => {
  fakeAttract({ 'blogPosts.listBlogs': factories.BlogList({ results: [{ title: 'Hello' }] }) })
  ace.ui.switchMode('raw')
  cleanup(() => ace.ui.switchMode('normal'))

  const command = await ace.create(LatestPosts, [])
  await command.exec()

  command.assertSucceeded()
  command.assertLog('[ blue(info) ] Hello')
})
```

The command is on [Ace commands](https://docs.fetchski-attract.com/adonisjs/ace-commands.md#commands-of-your-own). In `raw` mode Ace keeps
the command's output to assert on instead of printing it, its colors written as names. Commands live
outside `app/`, so import them by path. A command that prompts takes `command.prompt.trap('…').replyWith('…')`
for each question before `exec()`.

## A queue job

With `QUEUE_DRIVER=sync` in `.env.test`, `dispatch()` runs the job at once, through the container, so
the fake reaches it:

```ts tests/unit/sync_blog_posts.spec.ts theme={"dark"}
import { test } from '@japa/runner'
import { factories, fakeAttract, sequence } from '@fetchski/attract-cms-adonis/testing'
import SyncBlogPosts from '#jobs/sync_blog_posts'

test('walks every page', async () => {
  const site = fakeAttract({
    'blogPosts.listBlogs': sequence(
      factories.BlogList({ pages: 2 }),
      factories.BlogList({ pages: 2, currentPage: 2 })
    ),
  })

  await SyncBlogPosts.dispatch({ perPage: 50 })

  site.assertCalledTimes('blogPosts.listBlogs', 2)
})
```

The job is on [Jobs, cache and health](https://docs.fetchski-attract.com/adonisjs/background-work.md).

> [card] [Next: Ace commands](https://docs.fetchski-attract.com/adonisjs/ace-commands.md)
>
> Store a key, check a site, and a REPL with the client loaded.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.