Compare commits

..

5 Commits

Author SHA1 Message Date
Firehose Bot
3ffb0883f9 Fix review issues from commit 2a21d75
1. Rename goto_engineering_post_page/2 to visit_engineering_path/2 for
   better accuracy (used for both post pages and tag pages)

2. Simplify Makefile test target by removing explicit ecto.create and
   ecto.migrate commands (mix test handles migrations automatically)

3. Update blog_test.exs header comment to reflect actual changes made

4. Move Sandbox alias to top level in data_case.ex
2026-03-19 11:07:17 +00:00
Firehose Bot
2a21d75938 adjust makefile and refactor test 2026-03-18 20:04:41 +00:00
Firehose Bot
a82dab0350 also write something about unit tests 2026-03-18 20:03:41 +00:00
Firehose Bot
f05dd00c55 test writer skill
Focuses on integration tests, but might be more reusable
2026-03-18 20:02:20 +00:00
Firehose Bot
d428f51e8c Fix blog API tests and add missing tag tests
- Add Accept: application/json headers to all API endpoint tests
- Add GET /blog/releases/tag/:tag HTML page test
- Add GET /api/blog/*/tag/:tag JSON API tests for both blogs
- Fix feed.xml assertions to check body first, then content type
2026-03-18 19:03:40 +00:00
4 changed files with 209 additions and 24 deletions

View 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
```

View File

@ -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

View File

@ -1,28 +1,38 @@
# Firehose blog controller tests
defmodule FirehoseWeb.BlogTest do defmodule FirehoseWeb.BlogTest do
use FirehoseWeb.ConnCase use FirehoseWeb.ConnCase
defp visit_engineering_page(conn, suffix \\ "") do
path = "/blog/engineering" <> suffix
conn = get(conn, path)
body = html_response(conn, 200)
assert body =~ "Engineering Blog"
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 describe "engineering blog (HTML)" do
test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do test "GET /blog/engineering returns HTML index with layout", %{conn: conn} do
conn = get(conn, "/blog/engineering") visit_engineering_page(conn)
body = html_response(conn, 200)
assert body =~ "Engineering Blog"
assert body =~ "Hello World"
# Verify app layout is present (navbar)
assert body =~ "firehose"
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

View File

@ -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