Hex.pm Hexdocs CI License

Mutation-testing mutators for Swoosh, built as a plugin for Mutare.

Email code has a signature test gap: the suite asserts an email was sent, and nothing more. The wrong recipient class (cc where bcc protects privacy), a reply-to that quietly falls back to from, a dropped attachment, a provider template id nobody checks — or a deliver that never happens — all pass such a test. mutare_swoosh generates well-formed-but-wrong Swoosh programs at those points. A surviving mutant indicates a change that your test assertions did not detect.

Install

Add it (and Mutare) as dev/test dependencies:

def deps do
  [
    {:mutare, "~> 0.5.0", only: [:dev, :test], runtime: false},
    {:mutare_swoosh, "~> 0.2", only: [:dev, :test], runtime: false}
  ]
end

(Swoosh itself is not a dependency of this plugin — the mutators match calls purely syntactically. Your own project already supplies Swoosh.)

Enable

List the families in .mutare.exs, alongside Mutare's built-ins — naming your mailer activates the delivery family:

# .mutare.exs
[mutators: [:builtins] ++ Mutare.Swoosh.all(mailer: MyApp.Mailer)]

:builtins keeps Mutare's default families and adds the Swoosh ones. mailer: takes one module or a list; without it (Mutare.Swoosh.all()) the :swoosh_deliver family produces no mutations and everything else still runs. Mutants for all listed mailers are reported under swoosh_deliver — to report mailers separately, list the family twice with as: ({Mutare.Swoosh.Deliver, mailer: MyApp.AdminMailer, as: :swoosh_deliver_admin}). Each family has its own report name and can be enabled on its own:

[mutators: [:builtins, Mutare.Swoosh.Recipient]]   # just the recipient mutations

Then run Mutare as usual:

mix mutare

The families

FamilyNameMutationTest gap indicated by a survivor
Mutare.Swoosh.Deliver:swoosh_deliverreplaces Mailer.deliver/1,2 with a non-delivering {:ok, %{}} (deliver!/1,2 → %{}) on the configured mailer(s)no test asserts the email was actually sent — assert_email_sent
Mutare.Swoosh.Recipient:swoosh_recipientswaps recipient classes (to/cc/bcc; put_to/put_cc/put_bcc), deletes one recipient from literal values, and weakens put_* → plain (replace became append)no test checks the recipients or their class — the cc↔bcc swap affects recipient privacy
Mutare.Swoosh.Sender:swoosh_senderswaps from ↔ reply_to; drops reply_toreplies silently go to from; no test asserts the reply-to address
Mutare.Swoosh.Subject:swoosh_subjectblanks the subject to ""no test reads the subject
Mutare.Swoosh.Body:swoosh_bodyswaps html_body ↔ text_bodyno test distinguishes which body part carries the content
Mutare.Swoosh.Header:swoosh_headerremoves header/3; drops entries from a headers: optionno test asserts the custom header
Mutare.Swoosh.Attachment:swoosh_attachmentswaps disposition (type: :inline ↔ :attachment); removes the attachment entirelyno test asserts the attachment exists, or how it displays
Mutare.Swoosh.ProviderOption:swoosh_provider_optionremoves put_provider_option/3no test asserts the provider option — such as template ids or dynamic template data

Every email-field family mutates both the pipeline call (email |> subject("Welcome")) and the matching Swoosh.Email.new/1 option (Email.new(subject: "Welcome")). Calls match written qualified, aliased, imported, or as a pipe stage, and every replacement is itself a valid Swoosh program — a survivor means a missing assertion, not a crash.

Ignoring one kind of mutant

The families declare ignore-variant labels, so a # mutare:ignore[family:label] directive can suppress one kind of mutant without disabling the whole family:

email |> bcc(auditors)              # mutare:ignore[swoosh_recipient:cc] audit copy may be visible
email |> put_to(recipients)         # mutare:ignore[swoosh_recipient:append]
Mailer.deliver!(email)              # mutare:ignore[swoosh_deliver:deliver!]
email |> attachment(invoice_pdf)    # mutare:ignore[swoosh_attachment:delete]

The put_* → plain weakening (append) is deliberately one-directional: using put_to explicitly replaces existing recipients, so tests should check that replacement; plain to only appends recipients, so there is no reverse mutant. The two operations are equivalent when no recipients were set yet, so each such mutant includes a report note explaining that a kill may require pre-existing recipients.

What's deliberately out of scope

  • from removal — Swoosh validates the sender at delivery, so the mutant would only crash: an uninformative kill, not a test-quality signal.
  • Template rendering — render_body/3 and related functions are defined in phoenix_swoosh, and are covered by the companion mutare_phoenix_swoosh package.

Development

The test suite runs against stand-in Swoosh.Email/Swoosh.Attachment modules (test/support/swoosh_stubs.ex) that mirror the real API — kept faithful by hand, since a real :swoosh test dep would collide with them.

mix deps.get
mix test          # unit diffs + live semantic checks that a mutant actually changes behaviour
mix check         # format + credo + dialyzer

License

MIT — see LICENSE.