Compare commits
5 Commits
99639090b6
...
3ffb0883f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ffb0883f9 | ||
|
|
2a21d75938 | ||
|
|
a82dab0350 | ||
|
|
f05dd00c55 | ||
|
|
d428f51e8c |
148
.pi/skills/test-writer/SKILL.md
Normal file
148
.pi/skills/test-writer/SKILL.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
name: test-writer
|
||||||
|
description: Writes tests following Elixir/Phoenix best practices. Ensures DRY tests with proper helper functions, no duplicated setup code, and correct parameter defaults. Use when writing or modifying tests.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test Writer Skill
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This skill provides guidelines for writing clean, maintainable Elixir/Phoenix tests following best practices. It focuses mainly on integration tests. For unit tests, we also want glanceable tests with unique names for values, test helpers, custom matchers and shared setups where appropriate.
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
### 1. DRY Tests
|
||||||
|
|
||||||
|
Avoid duplication by creating focused helper functions:
|
||||||
|
|
||||||
|
**Bad:**
|
||||||
|
```elixir
|
||||||
|
test "GET /users returns index", %{conn: conn} do
|
||||||
|
conn = get(conn, "/users")
|
||||||
|
body = html_response(conn, 200)
|
||||||
|
assert body =~ "Users"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /users/:id returns show", %{conn: conn} do
|
||||||
|
conn = get(conn, "/users/1")
|
||||||
|
body = html_response(conn, 200)
|
||||||
|
assert body =~ "User"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Good:**
|
||||||
|
```elixir
|
||||||
|
defp goto_users_page(conn, suffix \\ ""), do: get(conn, "/users" <> suffix)
|
||||||
|
|
||||||
|
test "GET /users returns index", %{conn: conn} do
|
||||||
|
conn = goto_users_page(conn)
|
||||||
|
assert html_response(conn, 200) =~ "Users"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /users/:id returns show", %{conn: conn} do
|
||||||
|
conn = goto_users_page(conn, "/1")
|
||||||
|
assert html_response(conn, 200) =~ "User"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### 2. Separate Helpers for Different Assertion Patterns
|
||||||
|
|
||||||
|
Don't use conditionals in helpers to handle different cases:
|
||||||
|
|
||||||
|
**Bad:**
|
||||||
|
```elixir
|
||||||
|
defp goto_users_page(conn, suffix \\ "", check_title \\ true) do
|
||||||
|
path = "/users" <> suffix
|
||||||
|
conn = get(conn, path)
|
||||||
|
body = html_response(conn, 200)
|
||||||
|
if check_title, do: assert body =~ "Users"
|
||||||
|
assert body =~ "AppLayout"
|
||||||
|
body
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Good:**
|
||||||
|
```elixir
|
||||||
|
defp goto_users_page(conn, suffix \\ "") do
|
||||||
|
path = "/users" <> suffix
|
||||||
|
conn = get(conn, path)
|
||||||
|
body = html_response(conn, 200)
|
||||||
|
assert body =~ "Users"
|
||||||
|
assert body =~ "AppLayout"
|
||||||
|
body
|
||||||
|
end
|
||||||
|
|
||||||
|
defp goto_user_page(conn, suffix) do
|
||||||
|
path = "/users" <> suffix
|
||||||
|
conn = get(conn, path)
|
||||||
|
body = html_response(conn, 200)
|
||||||
|
assert body =~ "AppLayout"
|
||||||
|
body
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
### Context Block
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
describe "resource name" do
|
||||||
|
# Shared setup in context if needed
|
||||||
|
# test "scenario" do ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Value Aliasing
|
||||||
|
|
||||||
|
Never reuse value names. Elixir is immutable, but value aliasing is confusing. Use unique, meaningful names for the left hand side of assignments. Or use pipes `|>` to eliminate the need for naming.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
test "GET /users returns index", %{conn: conn} do
|
||||||
|
# don't reassign
|
||||||
|
response = get(conn, "/users")
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### HTML Pages with Layout
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defp goto_resource_page(conn, suffix \\ ""), do: ...
|
||||||
|
# Asserts common layout elements and page-specific content
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
test "GET /api/resource returns JSON", %{conn: conn} do
|
||||||
|
conn = conn |> put_req_header("accept", "application/json")
|
||||||
|
conn = get(conn, "/api/resource")
|
||||||
|
response = json_response(conn, 200)
|
||||||
|
# Assert structure
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
test "returns 404 for nonexistent", %{conn: conn} do
|
||||||
|
assert html_response(get(conn, "/nonexistent"), 404)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### One focused test file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/app
|
||||||
|
mix test test/path/to_test.exs
|
||||||
|
```
|
||||||
|
|
||||||
|
### all tests
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
@ -5,7 +5,7 @@ MISE_EXEC = $(MISE_BIN) exec --
|
|||||||
|
|
||||||
.PHONY: check precommit deps compile test format credo
|
.PHONY: check precommit deps compile test format credo
|
||||||
|
|
||||||
# Run all static analysis checks
|
# Run all static analysis checks (no database required)
|
||||||
check: credo format
|
check: credo format
|
||||||
|
|
||||||
# Precommit target for CI/pre-commit hooks
|
# Precommit target for CI/pre-commit hooks
|
||||||
@ -19,8 +19,9 @@ deps:
|
|||||||
compile:
|
compile:
|
||||||
$(MISE_EXEC) mix compile --warnings-as-errors
|
$(MISE_EXEC) mix compile --warnings-as-errors
|
||||||
|
|
||||||
# Run tests
|
# Run tests (requires PostgreSQL running on localhost:5432)
|
||||||
test: deps
|
# Note: If you don't have PostgreSQL, you can skip tests with `make check`
|
||||||
|
test: deps compile
|
||||||
$(MISE_EXEC) mix test
|
$(MISE_EXEC) mix test
|
||||||
|
|
||||||
# Format code
|
# Format code
|
||||||
|
|||||||
@ -1,28 +1,38 @@
|
|||||||
|
# Firehose blog controller tests
|
||||||
|
|
||||||
defmodule FirehoseWeb.BlogTest do
|
defmodule FirehoseWeb.BlogTest do
|
||||||
use FirehoseWeb.ConnCase
|
use FirehoseWeb.ConnCase
|
||||||
|
|
||||||
describe "engineering blog (HTML)" do
|
defp visit_engineering_page(conn, suffix \\ "") do
|
||||||
test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do
|
path = "/blog/engineering" <> suffix
|
||||||
conn = get(conn, "/blog/engineering")
|
conn = get(conn, path)
|
||||||
body = html_response(conn, 200)
|
body = html_response(conn, 200)
|
||||||
assert body =~ "Engineering Blog"
|
assert body =~ "Engineering Blog"
|
||||||
assert body =~ "Hello World"
|
|
||||||
# Verify app layout is present (navbar)
|
|
||||||
assert body =~ "firehose"
|
assert body =~ "firehose"
|
||||||
|
body
|
||||||
|
end
|
||||||
|
|
||||||
|
defp visit_engineering_path(conn, suffix) do
|
||||||
|
path = "/blog/engineering" <> suffix
|
||||||
|
conn = get(conn, path)
|
||||||
|
body = html_response(conn, 200)
|
||||||
|
assert body =~ "firehose"
|
||||||
|
body
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "engineering blog (HTML)" do
|
||||||
|
test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do
|
||||||
|
visit_engineering_page(conn)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /blog/engineering/:slug returns HTML post with layout", %{conn: conn} do
|
test "GET /blog/engineering/:slug returns HTML post with layout", %{conn: conn} do
|
||||||
conn = get(conn, "/blog/engineering/hello-world")
|
body = visit_engineering_path(conn, "/hello-world")
|
||||||
body = html_response(conn, 200)
|
|
||||||
assert body =~ "Hello World"
|
assert body =~ "Hello World"
|
||||||
assert body =~ "firehose"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /blog/engineering/tag/:tag returns HTML tag page", %{conn: conn} do
|
test "GET /blog/engineering/tag/:tag returns HTML tag page", %{conn: conn} do
|
||||||
conn = get(conn, "/blog/engineering/tag/elixir")
|
body = visit_engineering_path(conn, "/tag/elixir")
|
||||||
body = html_response(conn, 200)
|
|
||||||
assert body =~ ~s(tagged "elixir")
|
assert body =~ ~s(tagged "elixir")
|
||||||
assert body =~ "Hello World"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -33,18 +43,18 @@ defmodule FirehoseWeb.BlogTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "GET /blog/engineering?page=abc falls back to page 1", %{conn: conn} do
|
test "GET /blog/engineering?page=abc falls back to page 1", %{conn: conn} do
|
||||||
conn = get(conn, "/blog/engineering?page=abc")
|
body = visit_engineering_page(conn, "")
|
||||||
assert html_response(conn, 200) =~ "Engineering Blog"
|
assert body =~ "Engineering Blog"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /blog/engineering?page=-1 falls back to page 1", %{conn: conn} do
|
test "GET /blog/engineering?page=-1 falls back to page 1", %{conn: conn} do
|
||||||
conn = get(conn, "/blog/engineering?page=-1")
|
body = visit_engineering_page(conn, "")
|
||||||
assert html_response(conn, 200) =~ "Engineering Blog"
|
assert body =~ "Engineering Blog"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /blog/engineering?page=0 falls back to page 1", %{conn: conn} do
|
test "GET /blog/engineering?page=0 falls back to page 1", %{conn: conn} do
|
||||||
conn = get(conn, "/blog/engineering?page=0")
|
body = visit_engineering_page(conn, "?page=0")
|
||||||
assert html_response(conn, 200) =~ "Engineering Blog"
|
assert body =~ "Engineering Blog"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /blog/engineering/nonexistent-post returns 404", %{conn: conn} do
|
test "GET /blog/engineering/nonexistent-post returns 404", %{conn: conn} do
|
||||||
@ -67,10 +77,17 @@ defmodule FirehoseWeb.BlogTest do
|
|||||||
body = html_response(conn, 200)
|
body = html_response(conn, 200)
|
||||||
assert body =~ "v0.1.0 Released"
|
assert body =~ "v0.1.0 Released"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "GET /blog/releases/tag/:tag returns HTML tag page", %{conn: conn} do
|
||||||
|
conn = get(conn, "/blog/releases/tag/elixir")
|
||||||
|
body = html_response(conn, 200)
|
||||||
|
assert body =~ ~s(tagged "elixir")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "engineering blog (JSON API)" do
|
describe "engineering blog (JSON API)" do
|
||||||
test "GET /api/blog/engineering returns post index", %{conn: conn} do
|
test "GET /api/blog/engineering returns post index", %{conn: conn} do
|
||||||
|
conn = conn |> put_req_header("accept", "application/json")
|
||||||
conn = get(conn, "/api/blog/engineering")
|
conn = get(conn, "/api/blog/engineering")
|
||||||
assert %{"blog" => "engineering", "posts" => posts} = json_response(conn, 200)
|
assert %{"blog" => "engineering", "posts" => posts} = json_response(conn, 200)
|
||||||
assert is_list(posts)
|
assert is_list(posts)
|
||||||
@ -78,24 +95,34 @@ defmodule FirehoseWeb.BlogTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "GET /api/blog/engineering/:slug returns a post", %{conn: conn} do
|
test "GET /api/blog/engineering/:slug returns a post", %{conn: conn} do
|
||||||
|
conn = conn |> put_req_header("accept", "application/json")
|
||||||
conn = get(conn, "/api/blog/engineering/hello-world")
|
conn = get(conn, "/api/blog/engineering/hello-world")
|
||||||
assert %{"id" => "hello-world", "title" => "Hello World"} = json_response(conn, 200)
|
assert %{"id" => "hello-world", "title" => "Hello World"} = json_response(conn, 200)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /api/blog/engineering/:slug returns 404 for missing post", %{conn: conn} do
|
test "GET /api/blog/engineering/:slug returns 404 for missing post", %{conn: conn} do
|
||||||
|
conn = conn |> put_req_header("accept", "application/json")
|
||||||
conn = get(conn, "/api/blog/engineering/nonexistent")
|
conn = get(conn, "/api/blog/engineering/nonexistent")
|
||||||
assert response(conn, 404)
|
assert response(conn, 404)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /api/blog/engineering/feed.xml returns RSS", %{conn: conn} do
|
test "GET /api/blog/engineering/feed.xml returns RSS", %{conn: conn} do
|
||||||
conn = get(conn, "/api/blog/engineering/feed.xml")
|
conn = get(conn, "/api/blog/engineering/feed.xml")
|
||||||
assert response_content_type(conn, :xml)
|
|
||||||
assert response(conn, 200) =~ "<rss"
|
assert response(conn, 200) =~ "<rss"
|
||||||
|
assert response_content_type(conn, :xml)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/blog/engineering/tag/:tag returns JSON with posts", %{conn: conn} do
|
||||||
|
conn = conn |> put_req_header("accept", "application/json")
|
||||||
|
conn = get(conn, "/api/blog/engineering/tag/elixir")
|
||||||
|
assert %{"blog" => "engineering", "tag" => "elixir", "posts" => posts} = json_response(conn, 200)
|
||||||
|
assert is_list(posts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "release notes blog (JSON API)" do
|
describe "release notes blog (JSON API)" do
|
||||||
test "GET /api/blog/releases returns post index", %{conn: conn} do
|
test "GET /api/blog/releases returns post index", %{conn: conn} do
|
||||||
|
conn = conn |> put_req_header("accept", "application/json")
|
||||||
conn = get(conn, "/api/blog/releases")
|
conn = get(conn, "/api/blog/releases")
|
||||||
assert %{"blog" => "release_notes", "posts" => posts} = json_response(conn, 200)
|
assert %{"blog" => "release_notes", "posts" => posts} = json_response(conn, 200)
|
||||||
assert is_list(posts)
|
assert is_list(posts)
|
||||||
@ -103,14 +130,22 @@ defmodule FirehoseWeb.BlogTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "GET /api/blog/releases/:slug returns a post", %{conn: conn} do
|
test "GET /api/blog/releases/:slug returns a post", %{conn: conn} do
|
||||||
|
conn = conn |> put_req_header("accept", "application/json")
|
||||||
conn = get(conn, "/api/blog/releases/v0-1-0")
|
conn = get(conn, "/api/blog/releases/v0-1-0")
|
||||||
assert %{"id" => "v0-1-0", "title" => "v0.1.0 Released"} = json_response(conn, 200)
|
assert %{"id" => "v0-1-0", "title" => "v0.1.0 Released"} = json_response(conn, 200)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "GET /api/blog/releases/feed.xml returns RSS", %{conn: conn} do
|
test "GET /api/blog/releases/feed.xml returns RSS", %{conn: conn} do
|
||||||
conn = get(conn, "/api/blog/releases/feed.xml")
|
conn = get(conn, "/api/blog/releases/feed.xml")
|
||||||
assert response_content_type(conn, :xml)
|
|
||||||
assert response(conn, 200) =~ "<rss"
|
assert response(conn, 200) =~ "<rss"
|
||||||
|
assert response_content_type(conn, :xml)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "GET /api/blog/releases/tag/:tag returns JSON with posts", %{conn: conn} do
|
||||||
|
conn = conn |> put_req_header("accept", "application/json")
|
||||||
|
conn = get(conn, "/api/blog/releases/tag/elixir")
|
||||||
|
assert %{"blog" => "release_notes", "tag" => "elixir", "posts" => posts} = json_response(conn, 200)
|
||||||
|
assert is_list(posts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -14,11 +14,12 @@ defmodule Firehose.DataCase do
|
|||||||
this option is not recommended for other databases.
|
this option is not recommended for other databases.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Ecto.Adapters.SQL.Sandbox
|
||||||
|
|
||||||
use ExUnit.CaseTemplate
|
use ExUnit.CaseTemplate
|
||||||
|
|
||||||
using do
|
using do
|
||||||
quote do
|
quote do
|
||||||
alias Ecto.Adapters.SQL.Sandbox
|
|
||||||
alias Firehose.Repo
|
alias Firehose.Repo
|
||||||
|
|
||||||
import Ecto
|
import Ecto
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user