josefrichter

josefrichter

Proper structuring of testable code in context

Hi, I have a function in context that reads value from other table before inserting a new record. Specifically, it checks class capacity before adding new enrollment into it.

See:

defmodule Enrol.Classes do
  
  def create_enrollment(%{"class_id" => class_id} = attrs \\ %{}) do
    # check if class capacity is not exhausted
    # and inserts only then, otherwise rolls back
    Repo.transaction(fn ->
      capacity = Repo.one!(from c in Class, where: c.id == ^class_id, select: c.capacity)
      count = Repo.one(from e in Enrollment, where: e.class_id == ^class_id, select: count(e.class_id))
      
      case count < capacity do
        true ->
          # maybe call insert_enrollment(attrs) from here?
          %Enrollment{}
          |> Enrollment.changeset(attrs)
          |> Repo.insert()
        false ->
          IO.puts "Class capacity EXCEEDED, rollback"
          Repo.rollback(:class_full)
      end
    end) # end transaction
  end
  
  # default generic function
  def create_enrollment(attrs \\ %{}) do
    %Enrollment{}
    |> Enrollment.changeset(attrs)
    |> Repo.insert()
  end
  
  # alternative (just renamed)
  def insert_enrollment(attrs \\ %{}) do
    %Enrollment{}
    |> Enrollment.changeset(attrs)
    |> Repo.insert()
  end
end

When writing unit tests for this context, this would mean creating a class fixture first, so that I can read its capacity, before testing insertion of the enrollment. I feel like the unit test should be more granular, and just check whether plain vanilla insertion works.

So my questions are

  1. should I split my function so that it does the checks and then calls the plain vanilla create function?
  2. should I test these two separately? and does it make sense to test each of them?
  3. do I need to rename the second one to insert_enrollment? because they have the same arguments, co calling create_enrollment in the first one would basically result in recursive call and infinite loop, right?

Thank you very much!

Most Liked

stefanchrobot

stefanchrobot

I would suggest writing the use cases for your app and translating them into tests. It’s much better to have the unit of the test be a business use case then a context, module, function or a table.

I don’t know the details of your domain and app, so I’m going to make a bunch of assumptions. I’d suggest something like this:

describe "enrolling into a class" do
  test "a student can enroll to a class" do
    admin = AdminUI.create_admin()
    class = AdminUI.create_class(admin)
    student = AdminUI.create_student(admin)
    
    enrollment = StudentUI.enroll_to_class(student, class)

    assert enrollment.success
  end

  test "a student cannot enroll if the class is full" do
    admin = AdminUI.create_admin()
    class = AdminUI.create_class(admin, capacity: 3)
    for i <- 1..3 do
      student = AdminUI.create_student(admin)
      StudentUI.enroll_to_class(student, class)
    end

    student = AdminUI.create_student(admin)
    enrollment = StudentUI.enroll_to_class(student, class)
    
    assert !enrollment.success
  end
end

AdminUI and StudentUI are test helpers that model the actions that admins and students can perform in your app. It’s up to you to decide what’s the best way to implement those: you can use the API if you have one, use context or insert straight into the DB if that makes sense.

This way, the tests are going to be disconnected from the implementation and you’ll have much greater freedom in changing things if needed.

csadewa

csadewa

hmmm, your question is more of code structuring, and there’s room for opinion & preference on it, so i would answer just like how i would prefer to do it:

  1. Yes, i generally like to split module which deals with low level data (Enrollment, Classes) with module that govern business logic. in this case you mix actual DB creation logic of enrollment with business logic,
  2. yes, you should test the business logic code part, but testing insert_enrollment is a bit dubious (for me anyway) as it’s really standard code from ecto, i usually trust ecto to do it’s job.
  3. if you do split the module, having create_enrollment would be better as it’s more standard.

just for example, here’s how business logic code might be:

defmodule Enrol.EnrolService do
  
  def enroll_student(student, class) do
    Repo.transaction(fn ->
      if Classes.class_have_existing_capacity?(class) do
        Enrollment.create_enrollment(attrs)
      else
        Repo.rollback(:class_full)
      end
  end)
end
  

josefrichter

josefrichter

shortly before midnight, but all tests written and passing now :tada: thank you all for help!

Where Next?

Popular in Questions Top

joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New

Other popular topics Top

jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 48475 226
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New

We're in Beta

About us Mission Statement