this repo has no description

ft: add webrings to footer

hauleth.dev dfe20200 4956157c

verified
Changed files
+362 -11
content
sass
templates
+4
config.toml
···
[extra.twitter]
site = "@hauleth"
creator = "@hauleth"
+
+
[[extra.webrings]]
+
name = "Beambloggers"
+
url = "https://beambloggers.com/"
+290
content/post/writing-tests.md
···
+
+++
+
date = 2023-11-20
+
title = "How do I write Elixir tests?"
+
+
[taxonomies]
+
tags = [
+
"elixir",
+
"testing",
+
"programming"
+
]
+
+++
+
+
This post was created for myself to codify some basic guides that I use while
+
writing tests. If you, my dear reader, read this then there is one important
+
thing for you to remember:
+
+
These are **guides** not *rules*. Each code base is different deviations and are
+
expected and *will* happen. Just use the thing between your ears.
+
+
## `@subject` module attribute for module under test
+
+
While writing test in Elixir it is not always obvious what we are testing. Just
+
imagine test like:
+
+
```elixir
+
test "foo should frobnicate when bar" do
+
bar = pick_bar()
+
+
assert :ok == MyBehaviour.foo(MyImplementation, bar)
+
end
+
```
+
+
It is not obvious at the first sight what we are testing. And it is pretty
+
simplified example, in real world it can became even harder to notice what is
+
module under test (MUT).
+
+
To resolve that I came up with a simple solution. I create module attribute
+
named `@subject` that points to the MUT:
+
+
```elixir
+
@subject MyImplementation
+
+
test "foo should frobnicate when bar" do
+
bar = pick_bar()
+
+
assert :ok == MyBehaviour.foo(@subject, bar)
+
end
+
```
+
+
Now it is more obvious what is MUT and what is just wrapper code around it.
+
+
In the past I have been using `alias` with `:as` option, like:
+
+
```elixir
+
alias MyImplementation, as: Subject
+
```
+
+
However I find module attribute to be more visually outstanding and make it
+
easier for me to notice `@subject` than `Subject`. But your mileage may vary.
+
+
## `describe` with function name
+
+
That one is pretty basic, and I have seen that it is pretty standard for people:
+
when you are writing tests for module functions, then group them in `describe`
+
blocks that will contain name (and arity) of the function in the name. Example:
+
+
```elixir
+
# Module under test
+
defmodule Foo do
+
def a(x, y, z) do
+
# some code
+
end
+
end
+
+
# Tests
+
defmodule FooTest do
+
use ExUnit.Case, async: true
+
+
@subject Foo
+
+
describe "a/3" do
+
# Some tests here
+
end
+
end
+
```
+
+
This allows me to see what functionality I am testing.
+
+
Of course that doesn't apply to the Phoenix controllers, as there we do not test
+
functions, but tuples in form `{method, path}` which I then write as `METHOD
+
path`, for example `POST /users`.
+
+
## Avoid module mocking
+
+
In Elixir we have bunch of the mocking libraries out there, but most of them
+
have quite substantial issue for me - these prevent me from using `async: true`
+
for my tests. This often causes substantial performance hit, as it prevents
+
different modules to run in parallel (not single tests, *modules*, but that is
+
probably material for another post).
+
+
Instead of mocks I prefer to utilise dependency injection. Some people may argue
+
that "Elixir is FP, not OOP, there is no need for DI" and they cannot be further
+
from truth. DI isn't related to OOP, it just have different form, called
+
function arguments. For example, if we want to have function that do something
+
with time, in particular - current time. Then instead of writing:
+
+
```elixir
+
def my_function(a, b) do
+
do_foo(a, b, DateTime.utc_now())
+
end
+
```
+
+
Which would require me to use mocks for `DateTime` or other workarounds to make
+
tests time-independent. I would do:
+
+
```elixir
+
def my_function(a, b, now \\ DateTime.utc_now()) do
+
do_foo(a, b, now)
+
end
+
```
+
+
Which still provide me the ergonomics of `my_function/2` as above, but is way
+
easier to test, as I can pass the date to the function itself. This allows me to
+
run this test in parallel as it will not cause other tests to do weird stuff
+
because of altered `DateTime` behaviour.
+
+
## Avoid `ex_machina` factories
+
+
I have poor experience with tools like `ex_machina` or similar. These often
+
bring whole [Banana Gorilla Jungle problem][bgj] back, just changed a little, as
+
now instead of just passing data around, we create all needless structures for
+
sole purpose of test, even when they aren't needed for anything.
+
+
[bgj]: https://softwareengineering.stackexchange.com/q/368797
+
+
Start with example from [ExMachina README](https://github.com/beam-community/ex_machina#overview):
+
+
```elixir
+
defmodule MyApp.Factory do
+
# with Ecto
+
use ExMachina.Ecto, repo: MyApp.Repo
+
+
# without Ecto
+
use ExMachina
+
+
def user_factory do
+
%MyApp.User{
+
name: "Jane Smith",
+
email: sequence(:email, &"email-#{&1}@example.com"),
+
role: sequence(:role, ["admin", "user", "other"]),
+
}
+
end
+
+
def article_factory do
+
title = sequence(:title, &"Use ExMachina! (Part #{&1})")
+
# derived attribute
+
slug = MyApp.Article.title_to_slug(title)
+
%MyApp.Article{
+
title: title,
+
slug: slug,
+
# associations are inserted when you call `insert`
+
author: build(:user),
+
}
+
end
+
+
# derived factory
+
def featured_article_factory do
+
struct!(
+
article_factory(),
+
%{
+
featured: true,
+
}
+
)
+
end
+
+
def comment_factory do
+
%MyApp.Comment{
+
text: "It's great!",
+
article: build(:article),
+
author: build(:user) # That line is added by me
+
}
+
end
+
end
+
```
+
+
For start we can see a single problem there - we do not validate our factories
+
against our schema changesets. Without additional tests like:
+
+
```elixir
+
@subject MyApp.Article
+
+
test "factory conforms to changeset" do
+
changeset = @subject.changeset(%@subject{}, params_for(:article))
+
+
assert changeset.valid?
+
end
+
```
+
+
We cannot be sure that our tests test what we want them to test. And if we pass
+
custom attribute values in some tests it gets even worse, because we cannot be
+
sure if these are conforming either.
+
+
That mean that our tests may be moot, because we aren't testing against real
+
situations, but against some predefined state.
+
+
Another problem is that if we need to alter the behaviour of the factory it can
+
became quite convoluted. Imagine situation when we want to test if comments by
+
author of the post have some special behaviour (for example it has some
+
additional CSS class to be able to mark them in CSS). That require from us to do
+
some dancing around passing custom attributes:
+
+
```elixir
+
test "comments by author are special" do
+
post = insert(:post)
+
comment = insert(:comment, post: post, author: post.author)
+
+
# rest of the test
+
end
+
```
+
+
And this is simplified example. In the past I needed to deal with situations
+
where I was creating a lot of data to pass through custom attributes to make
+
test sensible.
+
+
Instead I prefer to do stuff directly in code. Instead of relying on some
+
"magical" functions provided by some "magical" macros from external library I
+
can use what I already have - functions in my application.
+
+
Instead of:
+
+
```elixir
+
test "comments by author are special" do
+
post = insert(:post)
+
comment = insert(:comment, post: post, author: post.author)
+
+
# rest of the test
+
end
+
```
+
+
Write:
+
+
```elixir
+
test "comments by author are special" do
+
author = MyApp.Users.create(%{
+
name: "John Doe",
+
email: "john@example.com"
+
})
+
post = MyApp.Blog.create_article(%{
+
author: author,
+
content: "Foo bar",
+
title: "Foo bar"
+
})
+
comment = MyApp.Blog.create_comment_for(article, %{
+
author: author,
+
content: "Foo bar"
+
})
+
+
# rest of the test
+
end
+
```
+
+
It may be a little bit more verbose, but it makes tests way more readable in my
+
opinion. You have all details just in place and you know what to expect. And if
+
you need some piece of data in all (or almost all) tests within
+
module/`describe` block, then you can always can use `setup/1` blocks. Or you
+
can create function per module that will generate data for you. As long as your
+
test module is self-contained and do not receive "magical" data out of thin air,
+
it is ok for me. But `ex_machina` is, in my opinion, terrible idea brought from
+
Rails world, that make little to no sense in Elixir.
+
+
If you really need such factories, then just write your own functions that will
+
use your contexts instead of relying on another library. For example:
+
+
```elixir
+
import ExUnit.Assertions
+
+
def create_user(name, email \\ nil, attrs \\ %{}) do
+
email = email || "#{String.replace(name, " ", ".")}@example.com"
+
attrs = Map.merge(attrs, %{name: name, email: email})
+
+
assert {:ok, user} = MyApp.Users.create(attrs)
+
+
user
+
end
+
+
# And so on…
+
```
+
+
This way you do not need to check if all tests use correct validations any
+
longer, as your system will do that for you.
+7 -7
flake.lock
···
"systems": "systems"
},
"locked": {
-
"lastModified": 1687709756,
-
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
+
"lastModified": 1705309234,
+
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
-
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
+
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
···
},
"nixpkgs": {
"locked": {
-
"lastModified": 1687103638,
-
"narHash": "sha256-dwy/TK6Db5W7ivcgmcxUykhFwodIg0jrRzOFt7H5NUc=",
-
"path": "/nix/store/p57nnwjhfvmsn75y9l6hn00pl2xv7ivm-source",
-
"rev": "91430887645a0953568da2f3e9a3a3bb0a0378ac",
+
"lastModified": 1704842529,
+
"narHash": "sha256-OTeQA+F8d/Evad33JMfuXC89VMetQbsU4qcaePchGr4=",
+
"path": "/nix/store/g16z4fs1mrbkxc4x6wm8xbrh13nc7aw4-source",
+
"rev": "eabe8d3eface69f5bb16c18f8662a702f50c20d5",
"type": "path"
},
"original": {
+40
sass/rings.scss
···
+
.rings {
+
margin-top: 1rem;
+
text-align: center;
+
+
details > summary {
+
list-style: none;
+
cursor: pointer;
+
+
&::before, &::after {
+
margin: 0 .5rem;
+
}
+
+
&::before { content: '▶'; }
+
&::after { content: '◀'; }
+
+
&::-webkit-details-marker {
+
display: none;
+
}
+
}
+
+
details[open] {
+
summary {
+
&::before, &::after {
+
content: '▼';
+
}
+
+
margin-bottom: 1rem;
+
}
+
}
+
+
ul {
+
list-style: none;
+
margin: 0;
+
}
+
+
li {
+
margin: 0;
+
padding: 0;
+
}
+
}
+1
sass/style.scss
···
@import 'post';
@import 'pagination';
@import 'footer';
+
@import 'rings';
:root {
--phoneWidth: (max-width: #{$phone-max-width + 1px});
+4 -4
templates/index.html
···
{% extends "zerm/templates/index.html" %}
{% block fonts %}
-
{% endblock %}
+
{% endblock fonts %}
{% block rss %}
{%- if config.generate_feed -%}
<link rel="alternate" type="application/atom+xml" title="{{ config.title }} Feed" href="{{ get_url(path=config.feed_filename) | safe}}">
{%- endif -%}
-
{% endblock %}
+
{% endblock rss %}
{% block og_preview %}
{{ social::og_preview() }}
···
{%- if config.extra.webmention -%}
<link rel="webmention" href="{{ config.extra.webmention }}" >
{%- endif -%}
-
{% endblock %}
+
{% endblock og_preview %}
{% block copyright %}
<div class="copyright">
···
</ul>
</nav>
</header>
-
{% endblock %}
+
{% endblock header %}
+16
templates/macros/extended_footer.html
···
+
{% macro extended_footer() %}
+
<div class="rings">
+
<details>
+
<summary>Webrings</summary>
+
<ul>
+
{%- for ring in config.extra.webrings -%}
+
<li>
+
<a href="{{ ring.url }}/prev?referrer={{ get_url(path = "/") | safe }}">&laquo;</a>
+
<a href="{{ ring.url }}">{{ ring.name }}</a>
+
<a href="{{ ring.url }}/next?referrer={{ get_url(path = "/") | safe }}">&raquo;</a>
+
</li>
+
{%- endfor -%}
+
</ul>
+
</details>
+
</div>
+
{% endmacro extended_footer %}