Engineering

How I changed the default structure of Elixir project to suit my preferences more

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 of test 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.