Reasonable defaults can make your life easier. I think that most programmers agree with that. Does that mean we should stick to them forever? Can a particular solution be suitable for everything and everyone?
This article tells the story of my experiment, which proved that sometimes it is worth breaking well-established standards.
To get some context
Let's start with the standard elixir project.
.
├── config
│ └── config.exs
├── lib
│ ├── foo
│ │ ├── bar.ex
│ │ └── baz.ex
│ └── foo.ex
├── mix.exs
└── test
├── foo
│ ├── bar_test.exs
│ └── baz_test.exs
├── foo_test.exs
└── test_helper.exs
It's the output of mix new foo
with two additional modules: Foo.Bar
and Foo.Baz
added manually. You can imagine this is your favorite small open-source library.
In my opinion, it has the following advantages:
- compiles the content of
lib
out of box - has everything that is needed to run tests
- looks familiar to many developers
- is easy to be released to hex (the content of
lib
is added to the package, the content oftest
isn't)
That's great. So... what's your problem?
I don't like having separate lib
and test
folders in phoenix projects. Ones that are big and not meant to be pushed to hex.
I'm bad at remembering shortcuts and names. On the other hand, I'm pretty good at remembering where individual files are stored (especially when the project has a clear division into small contexts). However, I don't want to go through the second tree with the same structure to get to the tests. It has always been a bit annoying.
One day while reading the documentation of mix test
task, I found out the following options: :test_paths
and :test_pattern
. The mere fact that such options exist in Elixir codebase means that they were useful to someone. I decided to check if they would also be useful for me.
First, I changed :test_paths
value to ["lib"]
. It allowed me to keep a file with tests next to the file with implementation. It also forced me to move .test_helper.exs
to the root of lib
, but one additional file isn't a problem. What bothered me more was the alphabetical order. Of two files with the same prefix, the one ending with _test.exs
was higher than its .ex
counterpart. It made the files tree in my editor look weird. Luckily, it was easy to fix by changing :test_pattern
value to "*.test.exs"
. From now on, files with tests had to be named like this: my_module.test.exs
.
.
├── config
│ └── config.exs
├── lib
│ ├── foo
│ │ ├── bar.ex
│ │ ├── bar.test.exs
│ │ ├── baz.ex
│ │ └── baz.test.exs
│ ├── foo.ex
│ ├── foo.test.exs
│ └── test_helper.exs
└── mix.exs
I was pleased with what I was able to achieve.
Ok. Is that all?
No.
After a few weeks of work in this way, new advantages appeared:
- Code review has become easier because changes to tests are right under changes to implementation (long-gone days of repeatedly scrolling the whole merge request on GitLab up and down).
- Faster refactoring (move single folder when changing namespaces)
- Well-written tests are a form of documentation. It is good to have them in an easily accessible place.
- It's visible if a module is important or not. The file generated by a framework or library doesn't have an additional
.test.exs
file associated with it.
It also has some disadvantages:
- I have to set manually
:test_paths
and:test_pattern
for every new umbrella application. - It may not be obvious where to put integration tests when there is no
test
folder.
However, these are minor ones comparing to how more comfortable my work has become.
I'm aware this topic may be controversial for some people. I encourage you to leave me a comment if you think that I've missed something.
Did you find the article interesting and helpful? Take a look at our Elixir page to find out more!
#language #programming #framework