Co-locating spec files within src directory


#1

Now I’m not expecting (m)any of you to agree that this is a good idea but it’s something I’ve picked up from the last couple of years working in the JS world and it’s something I’m going to carry forward with my Crystal projects.

The key difference is that instead of:

spec/
  models/
    user_spec.cr
src/
  models/
    user.cr

You now have:

src/
  __spec__/
    user_spec.cr
  models/
    user.cr

And I can get this working without too much issue (the spec files are required in when using require "./**" but I can live with this). The problem I’m currently facing is making this somewhat elegant due to the following issues:

  • require "spec" adds methods to the global scope, which I don’t want when not running tests.
  • require "spec" will also launch the spec runner on require.

Can anyone think of a better way of handling this than just:

{% if env("ENV") == "test" %}
  require "spec"

  describe Class do
  end
{% end %}

in all my spec files?


#2

Why not just not include the spec files in your main file?

Like require "./src/models/**" vs require "./src/**" so that you wont have to worry about that. Then you want to run your specs just do like crystal spec src/__spec__.

Normally you also have a spec_helper.cr file that requires spec and your app’s files.


#3

Valid point. I can’t say that I’ve got a bigger reason than simply requiring each folder feels like it’s leaking more mess into the real source code than it’s worth. When the folders are deeply nested it kind of becomes a little unwieldly.

I’d be going from:

require "./lib/**"
require "./apps/**"

to having require statements for each level of nesting within each application as a __spec__ folder can live within src/apps/weblog/__spec__, src/apps/weblog/posts/__spec__, src/apps/weblog/posts/schemas/__spec__ (for example).

edit: rather than ending up with a huge main.cr I initially thought it was ‘less worse’ than wrapping each test in the env condition.


#4

TBH you’re just making more work for yourself. Why not just use spec/ directory like its meant to be used; then this becomes a non problem?


#5

Once you go colocated spec directory you never go back, as the saying goes


#6

Fair enough. There isn’t a way to filter out files/directories with require. If you don’t want to manually build out the list of directories in your main file then using macro like that would be your only way afaik. Possibly could also use compile time flags to “group” your specs.

{% if flag?(:model) %}
require “spec”

describe User do
end
{% end %}

Which could then be ran like crystal spec src/ -Dmodel.

IMO still not worth the extra effort but :man_shrugging:

EDIT: https://crystal-lang.org/reference/syntax_and_semantics/compile_time_flags.html


#7

I actually rather like that above relying on env, I think I’ll incorporate it, thanks!


#8

You can also redefine describe in case the module Spec doesn’t exists e.g. in src/init.cr:

{% if !@type.constants.any? &.==("Spec")  %}
  macro describe(x, &block)
    # Ignore the code
  end
{%  end %}

You then keep your /spec folder, but just adding spec_helper.cr file like this:

require "spec"
require "../src/**"

Advantages:

  • Usage of crystal spec without refinement should still works :smile:
  • No boilerplate at all in your __spec__ files
  • Crystal will still complains if syntax inside describe is wrong. The AST note is constructed so some tools will still understand your code and give you advice. It’s not the case if you use the flag condition, the block is then completely ignored, no AST, no ameba, nothing until your run with the flag.

#9

This feels like another step up, and I’ve combined approaches for something way cleaner than I had in the first place. I’ve basically implemented your mock but within spec_helper. Unsure whether there was any advantage to macro over def here so stuck with what worked first time.

// spec_helper.cr
{% if flag?(:test) %}
  require "../dependencies"
  require "spec"
{% else %}
  macro describe(description, file = __FILE__, line = __LINE__, &block)
  end
{% end %}

Fairly pleased with where this is now, thanks to everyone for the suggestions.