Initial upload
This commit is contained in:
292
elixir/city-office/test/form_test.exs
Normal file
292
elixir/city-office/test/form_test.exs
Normal file
@@ -0,0 +1,292 @@
|
||||
defmodule FormTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# Dear Elixir learner,
|
||||
# If you're reading this test suite to gain some insights,
|
||||
# please be advised that it is somewhat unusual.
|
||||
#
|
||||
# Don't worry if you don't understand this test suite at this stage of your learning journey.
|
||||
# We had to use some advanced features to be able to write assertions about docs and typespecs.
|
||||
# You wouldn't normally write assertions for that in a typical codebase.
|
||||
# We're doing it here strictly for educational purposes.
|
||||
|
||||
defmacrop assert_moduledoc(expected_moduledoc) do
|
||||
quote do
|
||||
{:docs_v1, _, _, _, module_doc, _, _} = Code.fetch_docs(Form)
|
||||
|
||||
if module_doc == :none do
|
||||
flunk("expected the module Form to have documentation")
|
||||
else
|
||||
actual_moduledoc = module_doc["en"]
|
||||
assert actual_moduledoc == unquote(expected_moduledoc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop assert_doc({function_name, function_arity}, expected_doc) do
|
||||
quote do
|
||||
{:docs_v1, _, _, _, _, _, docs} = Code.fetch_docs(Form)
|
||||
|
||||
{_, _, _, doc_content, _} =
|
||||
Enum.find(docs, fn {{kind, function_name, arity}, _, _, _, _} ->
|
||||
{kind, function_name, arity} ==
|
||||
{:function, unquote(function_name), unquote(function_arity)}
|
||||
end)
|
||||
|
||||
if doc_content == :none do
|
||||
flunk(
|
||||
"expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have documentation"
|
||||
)
|
||||
else
|
||||
actual_doc = doc_content["en"]
|
||||
assert actual_doc == unquote(expected_doc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop assert_spec({function_name, function_arity}, arguments_specs, return_spec) do
|
||||
quote do
|
||||
{:ok, specs} = Code.Typespec.fetch_specs(Form)
|
||||
|
||||
spec =
|
||||
Enum.find(specs, fn {{function_name, arity}, _} ->
|
||||
{function_name, arity} == {unquote(function_name), unquote(function_arity)}
|
||||
end)
|
||||
|
||||
assert spec,
|
||||
"expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have a typespec"
|
||||
|
||||
{{unquote(function_name), unquote(function_arity)}, [{:type, _, :fun, _} = function_spec]} =
|
||||
spec
|
||||
|
||||
{:"::", _, [arguments, return]} =
|
||||
Code.Typespec.spec_to_quoted(unquote(function_name), function_spec)
|
||||
|
||||
accepted_arguments_specs =
|
||||
Enum.map(unquote(arguments_specs), fn arguments_spec ->
|
||||
"#{unquote(function_name)}(#{arguments_spec})"
|
||||
end)
|
||||
|
||||
actual_arguments_spec = Macro.to_string(arguments)
|
||||
assert actual_arguments_spec in accepted_arguments_specs
|
||||
|
||||
expected_return_spec = unquote(return_spec)
|
||||
actual_return_spec = Macro.to_string(return)
|
||||
assert actual_return_spec == expected_return_spec
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop assert_type({module_name, type_name}, expected_type_definition) do
|
||||
quote do
|
||||
{:ok, types} = Code.Typespec.fetch_types(unquote(module_name))
|
||||
|
||||
type =
|
||||
Enum.find(types, fn {declaration, {type_name, _, _}} ->
|
||||
declaration == :type && type_name == unquote(type_name)
|
||||
end)
|
||||
|
||||
assert type,
|
||||
"expected the module #{unquote(module_name)} to have a public type named #{unquote(type_name)}"
|
||||
|
||||
{:type, type} = type
|
||||
|
||||
{:"::", _, [_, type_definition]} = Code.Typespec.type_to_quoted(type)
|
||||
|
||||
actual_type_definition = Macro.to_string(type_definition)
|
||||
|
||||
if is_list(unquote(expected_type_definition)) do
|
||||
if actual_type_definition in unquote(expected_type_definition) do
|
||||
assert true
|
||||
else
|
||||
# we know this will fail at this point, but we're using it to provide a nice failure message
|
||||
assert actual_type_definition == hd(unquote(expected_type_definition))
|
||||
end
|
||||
else
|
||||
assert actual_type_definition == unquote(expected_type_definition)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "the Form module" do
|
||||
@tag task_id: 1
|
||||
test "has documentation" do
|
||||
expected_moduledoc = """
|
||||
A collection of loosely related functions helpful for filling out various forms at the city office.
|
||||
"""
|
||||
|
||||
assert_moduledoc(expected_moduledoc)
|
||||
end
|
||||
end
|
||||
|
||||
describe "blanks/1" do
|
||||
@tag task_id: 2
|
||||
test "returns a string with Xs of a given length" do
|
||||
assert Form.blanks(5) == "XXXXX"
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "returns an empty string when given length is 0" do
|
||||
assert Form.blanks(0) == ""
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "has documentation" do
|
||||
expected_doc = """
|
||||
Generates a string of a given length.
|
||||
|
||||
This string can be used to fill out a form field that is supposed to have no value.
|
||||
Such fields cannot be left empty because a malicious third party could fill them out with false data.
|
||||
"""
|
||||
|
||||
assert_doc({:blanks, 1}, expected_doc)
|
||||
end
|
||||
|
||||
@tag task_id: 2
|
||||
test "has a correct spec" do
|
||||
assert_spec({:blanks, 1}, ["n :: non_neg_integer()", "non_neg_integer()"], "String.t()")
|
||||
end
|
||||
end
|
||||
|
||||
describe "letters/1" do
|
||||
@tag task_id: 3
|
||||
test "returns a list of upcase letters" do
|
||||
assert Form.letters("Sao Paulo") == ["S", "A", "O", " ", "P", "A", "U", "L", "O"]
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "returns an empty list when given an empty string" do
|
||||
assert Form.letters("") == []
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "has documentation" do
|
||||
expected_doc = """
|
||||
Splits the string into a list of uppercase letters.
|
||||
|
||||
This is needed for form fields that don't offer a single input for the whole string,
|
||||
but instead require splitting the string into a predefined number of single-letter inputs.
|
||||
"""
|
||||
|
||||
assert_doc({:letters, 1}, expected_doc)
|
||||
end
|
||||
|
||||
@tag task_id: 3
|
||||
test "has a typespec" do
|
||||
assert_spec({:letters, 1}, ["word :: String.t()", "String.t()"], "[String.t()]")
|
||||
end
|
||||
end
|
||||
|
||||
describe "check_length/2" do
|
||||
@tag task_id: 4
|
||||
test "returns :ok is value is below max length" do
|
||||
assert Form.check_length("Ruiz", 6) == :ok
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "returns :ok is value is of exactly max length" do
|
||||
assert Form.check_length("Martinez-Cooper", 15) == :ok
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "returns an error tuple with the difference between max length and actual length" do
|
||||
assert Form.check_length("Martinez-Campbell", 10) == {:error, 7}
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "has documentation" do
|
||||
expected_doc = """
|
||||
Checks if the value has no more than the maximum allowed number of letters.
|
||||
|
||||
This is needed to check that the values of fields do not exceed the maximum allowed length.
|
||||
It also tells you by how much the value exceeds the maximum.
|
||||
"""
|
||||
|
||||
assert_doc({:check_length, 2}, expected_doc)
|
||||
end
|
||||
|
||||
@tag task_id: 4
|
||||
test "has a typespec" do
|
||||
assert_spec(
|
||||
{:check_length, 2},
|
||||
["word :: String.t(), length :: non_neg_integer()", "String.t(), non_neg_integer()"],
|
||||
":ok | {:error, pos_integer()}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "custom types in the Form module" do
|
||||
@tag task_id: 5
|
||||
test "has a custom 'address_map' type" do
|
||||
expected_type_definitions = [
|
||||
"%{street: String.t(), postal_code: String.t(), city: String.t()}",
|
||||
"%{street: String.t(), city: String.t(), postal_code: String.t()}",
|
||||
"%{postal_code: String.t(), street: String.t(), city: String.t()}",
|
||||
"%{postal_code: String.t(), city: String.t(), street: String.t()}",
|
||||
"%{city: String.t(), street: String.t(), postal_code: String.t()}",
|
||||
"%{city: String.t(), postal_code: String.t(), street: String.t()}"
|
||||
]
|
||||
|
||||
assert_type({Form, :address_map}, expected_type_definitions)
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "has a custom 'address_tuple' type with named arguments" do
|
||||
expected_type_definition =
|
||||
"{street :: String.t(), postal_code :: String.t(), city :: String.t()}"
|
||||
|
||||
assert_type({Form, :address_tuple}, expected_type_definition)
|
||||
end
|
||||
|
||||
@tag task_id: 5
|
||||
test "has a custom 'address' type that is a union of 'address_map' and 'address_tuple'" do
|
||||
expected_type_definitions = [
|
||||
"address_map() | address_tuple()",
|
||||
"address_tuple() | address_map()"
|
||||
]
|
||||
|
||||
assert_type({Form, :address}, expected_type_definitions)
|
||||
end
|
||||
end
|
||||
|
||||
describe "format_address/1" do
|
||||
@tag task_id: 6
|
||||
test "accepts a map" do
|
||||
input = %{
|
||||
street: "Wiejska 4/6/8",
|
||||
postal_code: "00-902",
|
||||
city: "Warsaw"
|
||||
}
|
||||
|
||||
result = """
|
||||
WIEJSKA 4/6/8
|
||||
00-902 WARSAW
|
||||
"""
|
||||
|
||||
assert Form.format_address(input) == result
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "accepts a 3 string tuple" do
|
||||
result = """
|
||||
PLATZ DER REPUBLIK 1
|
||||
11011 BERLIN
|
||||
"""
|
||||
|
||||
assert Form.format_address({"Platz der Republik 1", "11011", "Berlin"}) == result
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "has documentation" do
|
||||
expected_doc = """
|
||||
Formats the address as an uppercase multiline string.
|
||||
"""
|
||||
|
||||
assert_doc({:format_address, 1}, expected_doc)
|
||||
end
|
||||
|
||||
@tag task_id: 6
|
||||
test "has a typespec" do
|
||||
assert_spec({:format_address, 1}, ["address :: address()", "address()"], "String.t()")
|
||||
end
|
||||
end
|
||||
end
|
2
elixir/city-office/test/test_helper.exs
Normal file
2
elixir/city-office/test/test_helper.exs
Normal file
@@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
Reference in New Issue
Block a user