How I changed the default structure of Elixir project to suit my preferences more
by Tobiasz Małecki

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.

This website stores cookies on your computer. The data is used to collect information about how you interact with our website and allow us to remember you. We use this information to improve and customize your browsing experience and for analytics and metrics about our visitors both on this website and other media. Cookie Policy Privacy Notice
Work together

Let’s Work Together!

Make the first step for a great partnership! Share your idea with us and check what we can do for you and your company.
AppUnite Sp. z o.o.
VAT ID: PL 7831689686
Droga Dębińska 3A/3
61-555 Poznań, Poland
+48 532 568 641
office@appunite.com
Clutch Top Developers 2020Clutch Top B2B Companies 2019Financial Times ranking of 1000 fastest-growing companies in EuropeDiament Miesięcznika Forbes