Skip to content

Latest commit

 

History

History
837 lines (620 loc) · 14.1 KB

File metadata and controls

837 lines (620 loc) · 14.1 KB

Module 4: Working with Mix

🎯 Module Objectives

  • Understand what Mix is and its role
  • Create and structure Mix projects
  • Manage dependencies with Hex
  • Write and run tests with ExUnit
  • Build a complete CLI application
  • Understand compilation and releases

What is Mix?

Mix is Elixir's build tool that provides:

  • Project creation and management
  • Dependency management
  • Compilation
  • Testing
  • Task running
  • Release management

Think of Mix as Elixir's equivalent to npm, cargo, or gradle.


Creating a Mix Project

Basic Project

# Create a new project
mix new my_app

# Output:
# * creating README.md
# * creating .formatter.exs
# * creating .gitignore
# * creating mix.exs
# * creating lib
# * creating lib/my_app.ex
# * creating test
# * creating test/test_helper.exs
# * creating test/my_app_test.exs

Project with Supervisor

# Create project with supervision tree
mix new my_app --sup

# This adds an Application module

Project Structure

my_app/
├── .formatter.exs      # Code formatting configuration
├── .gitignore          # Git ignore file
├── README.md           # Project documentation
├── mix.exs             # Project configuration
├── lib/                # Application code
│   └── my_app.ex
└── test/               # Tests
    ├── test_helper.exs
    └── my_app_test.exs

mix.exs: Project Configuration

The mix.exs file defines your project:

defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,                    # Application name
      version: "0.1.0",                # Version
      elixir: "~> 1.15",               # Elixir version requirement
      start_permanent: Mix.env() == :prod,
      deps: deps()                     # Dependencies
    ]
  end

  # Run "mix help compile.app" for more
  def application do
    [
      extra_applications: [:logger]    # Extra OTP applications
    ]
  end

  # Run "mix help deps" for more
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end
end

Mix Commands

Essential Commands

# Compile project
mix compile

# Run project
mix run

# Start interactive shell with project loaded
iex -S mix

# Format code
mix format

# Clean build artifacts
mix clean

# Show help
mix help
mix help deps  # Help for specific task

Project Information

# List all tasks
mix help

# Show dependencies
mix deps

# Show dependency tree
mix deps.tree

# Project version
mix version

Dependencies with Hex

Hex.pm is Elixir's package manager.

Adding Dependencies

Edit mix.exs:

defp deps do
  [
    {:poison, "~> 5.0"},              # JSON library
    {:httpoison, "~> 2.0"},           # HTTP client
    {:timex, "~> 3.7"}                # Date/Time library
  ]
end

Version requirements:

  • ~> 1.0 - >= 1.0.0 and < 2.0.0
  • >= 1.0.0 - Any version >= 1.0.0
  • == 1.0.0 - Exact version
  • ~> 1.0.3 - >= 1.0.3 and < 1.1.0

Managing Dependencies

# Install/update dependencies
mix deps.get

# Update all dependencies
mix deps.update --all

# Update specific dependency
mix deps.update poison

# Show outdated dependencies
mix hex.outdated

# Clean unused dependencies
mix deps.clean --unused

Finding Packages

# Search Hex
mix hex.search json

# Get package info
mix hex.info poison

Testing with ExUnit

ExUnit is Elixir's built-in testing framework.

Basic Test Structure

test/my_app_test.exs:

defmodule MyAppTest do
  use ExUnit.Case
  doctest MyApp

  test "greets the world" do
    assert MyApp.hello() == :world
  end
end

Running Tests

# Run all tests
mix test

# Run specific test file
mix test test/my_app_test.exs

# Run specific test line
mix test test/my_app_test.exs:12

# Run with detailed output
mix test --trace

# Run with coverage
mix test --cover

# Watch mode (requires mix_test_watch)
mix test.watch

Test Assertions

defmodule MathTest do
  use ExUnit.Case

  test "addition" do
    assert 1 + 1 == 2
  end

  test "subtraction" do
    result = 5 - 3
    assert result == 2
  end

  test "refute (opposite of assert)" do
    refute 1 + 1 == 3
  end

  test "assert_raise" do
    assert_raise ArithmeticError, fn ->
      1 / 0
    end
  end

  test "assert_receive (for messages)" do
    send(self(), :hello)
    assert_receive :hello
  end
end

Test Organization

defmodule StringTest do
  use ExUnit.Case

  describe "String.upcase/1" do
    test "converts lowercase to uppercase" do
      assert String.upcase("hello") == "HELLO"
    end

    test "handles empty string" do
      assert String.upcase("") == ""
    end
  end

  describe "String.downcase/1" do
    test "converts uppercase to lowercase" do
      assert String.downcase("HELLO") == "hello"
    end
  end
end

Setup and Teardown

defmodule DatabaseTest do
  use ExUnit.Case

  # Runs once before all tests
  setup_all do
    # Start test database
    {:ok, pid} = TestDatabase.start_link()
    
    # Clean up after all tests
    on_exit(fn -> TestDatabase.stop(pid) end)
    
    %{db: pid}
  end

  # Runs before each test
  setup do
    # Reset database
    TestDatabase.clear()
    :ok
  end

  test "insert user", %{db: db} do
    # Test uses db from setup_all
    assert :ok = TestDatabase.insert(db, :user, %{name: "Alice"})
  end
end

Doctests

Test code examples in documentation:

defmodule Calculator do
  @doc """
  Adds two numbers.

  ## Examples

      iex> Calculator.add(2, 3)
      5

      iex> Calculator.add(-1, 1)
      0

  """
  def add(a, b), do: a + b
end

# In test file
defmodule CalculatorTest do
  use ExUnit.Case
  doctest Calculator  # This runs the examples above as tests
end

Practical Example: Calculator CLI

Let's build a complete CLI calculator application.

Step 1: Create Project

mix new calculator
cd calculator

Step 2: Implement Calculator Module

lib/calculator.ex:

defmodule Calculator do
  @moduledoc """
  A simple calculator module.
  """

  @doc """
  Adds two numbers.

  ## Examples

      iex> Calculator.add(2, 3)
      5

  """
  def add(a, b) when is_number(a) and is_number(b), do: a + b

  @doc """
  Subtracts two numbers.

  ## Examples

      iex> Calculator.subtract(5, 3)
      2

  """
  def subtract(a, b) when is_number(a) and is_number(b), do: a - b

  @doc """
  Multiplies two numbers.

  ## Examples

      iex> Calculator.multiply(4, 5)
      20

  """
  def multiply(a, b) when is_number(a) and is_number(b), do: a * b

  @doc """
  Divides two numbers.

  ## Examples

      iex> Calculator.divide(10, 2)
      {:ok, 5.0}

      iex> Calculator.divide(10, 0)
      {:error, :division_by_zero}

  """
  def divide(_a, 0), do: {:error, :division_by_zero}
  def divide(a, b) when is_number(a) and is_number(b), do: {:ok, a / b}

  @doc """
  Evaluates a mathematical expression.

  ## Examples

      iex> Calculator.evaluate("2 + 3")
      {:ok, 5}

      iex> Calculator.evaluate("10 / 2")
      {:ok, 5.0}

  """
  def evaluate(expression) do
    with {:ok, tokens} <- tokenize(expression),
         {:ok, result} <- calculate(tokens) do
      {:ok, result}
    end
  end

  defp tokenize(expression) do
    parts = String.split(expression, " ", trim: true)
    
    case parts do
      [a, op, b] ->
        with {num_a, _} <- Float.parse(a),
             {num_b, _} <- Float.parse(b) do
          {:ok, {num_a, op, num_b}}
        else
          _ -> {:error, :invalid_numbers}
        end
      _ ->
        {:error, :invalid_expression}
    end
  end

  defp calculate({a, "+", b}), do: {:ok, add(a, b)}
  defp calculate({a, "-", b}), do: {:ok, subtract(a, b)}
  defp calculate({a, "*", b}), do: {:ok, multiply(a, b)}
  defp calculate({a, "/", b}), do: divide(a, b)
  defp calculate(_), do: {:error, :unknown_operator}
end

Step 3: Create CLI Module

lib/calculator/cli.ex:

defmodule Calculator.CLI do
  @moduledoc """
  CLI interface for the calculator.
  """

  def main(args \\ []) do
    args
    |> parse_args()
    |> process()
  end

  defp parse_args(args) do
    case args do
      [] -> :interactive
      [expression] -> {:expression, expression}
      _ -> :help
    end
  end

  defp process(:help) do
    IO.puts("""
    Calculator CLI

    Usage:
      calculator "2 + 3"    # Evaluate expression
      calculator            # Interactive mode

    Supported operations: +, -, *, /
    """)
  end

  defp process(:interactive) do
    IO.puts("Calculator (type 'quit' to exit)")
    interactive_loop()
  end

  defp process({:expression, expr}) do
    case Calculator.evaluate(expr) do
      {:ok, result} ->
        IO.puts("Result: #{result}")
      {:error, reason} ->
        IO.puts("Error: #{reason}")
    end
  end

  defp interactive_loop do
    input = IO.gets("> ") |> String.trim()

    case input do
      "quit" ->
        IO.puts("Goodbye!")
      "" ->
        interactive_loop()
      expression ->
        case Calculator.evaluate(expression) do
          {:ok, result} ->
            IO.puts("= #{result}")
          {:error, reason} ->
            IO.puts("Error: #{reason}")
        end
        interactive_loop()
    end
  end
end

Step 4: Write Tests

test/calculator_test.exs:

defmodule CalculatorTest do
  use ExUnit.Case
  doctest Calculator

  describe "basic operations" do
    test "add/2" do
      assert Calculator.add(2, 3) == 5
      assert Calculator.add(-1, 1) == 0
    end

    test "subtract/2" do
      assert Calculator.subtract(5, 3) == 2
      assert Calculator.subtract(0, 5) == -5
    end

    test "multiply/2" do
      assert Calculator.multiply(4, 5) == 20
      assert Calculator.multiply(-2, 3) == -6
    end

    test "divide/2" do
      assert Calculator.divide(10, 2) == {:ok, 5.0}
      assert Calculator.divide(7, 2) == {:ok, 3.5}
    end

    test "divide by zero" do
      assert Calculator.divide(10, 0) == {:error, :division_by_zero}
    end
  end

  describe "evaluate/1" do
    test "evaluates addition" do
      assert Calculator.evaluate("2 + 3") == {:ok, 5}
    end

    test "evaluates subtraction" do
      assert Calculator.evaluate("10 - 7") == {:ok, 3}
    end

    test "evaluates multiplication" do
      assert Calculator.evaluate("4 * 5") == {:ok, 20}
    end

    test "evaluates division" do
      assert Calculator.evaluate("10 / 2") == {:ok, 5.0}
    end

    test "handles invalid expression" do
      assert Calculator.evaluate("invalid") == {:error, :invalid_expression}
    end

    test "handles division by zero" do
      assert Calculator.evaluate("5 / 0") == {:error, :division_by_zero}
    end
  end
end

Step 5: Configure as Escript

Update mix.exs:

def project do
  [
    app: :calculator,
    version: "0.1.0",
    elixir: "~> 1.15",
    start_permanent: Mix.env() == :prod,
    escript: escript(),  # Add this line
    deps: deps()
  ]
end

# Add this function
defp escript do
  [main_module: Calculator.CLI]
end

Step 6: Build and Run

# Run tests
mix test

# Build executable
mix escript.build

# Run executable
./calculator "10 + 5"
# Output: Result: 15

# Interactive mode
./calculator
# Calculator (type 'quit' to exit)
# > 5 * 3
# = 15
# > quit
# Goodbye!

Configuration

Config Files

Elixir supports environment-specific configuration:

config/
├── config.exs        # Base configuration
├── dev.exs           # Development
├── test.exs          # Testing
└── prod.exs          # Production

config/config.exs:

import Config

config :my_app,
  api_key: "dev_key",
  port: 4000

# Import environment specific config
import_config "#{config_env()}.exs"

config/prod.exs:

import Config

config :my_app,
  api_key: System.get_env("API_KEY"),
  port: 8080

Using Configuration

defmodule MyApp do
  def api_key do
    Application.get_env(:my_app, :api_key)
  end
end

Code Formatting

Mix includes a code formatter:

# Format all files
mix format

# Check if files are formatted
mix format --check-formatted

.formatter.exs:

[
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
  line_length: 100
]

Documentation

Generating Docs

Add ExDoc dependency:

defp deps do
  [
    {:ex_doc, "~> 0.30", only: :dev, runtime: false}
  ]
end

Generate documentation:

mix deps.get
mix docs

# Open docs/index.html in browser

Writing Documentation

defmodule MyModule do
  @moduledoc """
  This module does amazing things.
  
  ## Examples
  
      iex> MyModule.hello()
      "Hello"
  
  """
  
  @doc """
  Says hello.
  
  ## Parameters
  
  - name: String - The name to greet
  
  ## Examples
  
      iex> MyModule.hello("Alice")
      "Hello, Alice"
  
  """
  @spec hello(String.t()) :: String.t()
  def hello(name) do
    "Hello, #{name}"
  end
end

🎓 Module Summary

You have learned:

  • ✅ What Mix is and its core features
  • ✅ How to create and structure Mix projects
  • ✅ Managing dependencies with Hex
  • ✅ Writing tests with ExUnit
  • ✅ Building CLI applications
  • ✅ Configuration management
  • ✅ Code formatting and documentation

➡️ Next Step

Module 5: Introduction to Phoenix


💡 Practice Exercises

  1. Todo CLI: Build a command-line todo list manager

    • Add, list, complete, and delete tasks
    • Persist data to a file
    • Include full test suite
  2. Unit Converter: Create a unit conversion tool

    • Temperature (C, F, K)
    • Distance (m, km, mi, ft)
    • Weight (kg, lb, oz)
    • CLI interface
  3. CSV Parser: Build a CSV file parser

    • Parse CSV to list of maps
    • Handle headers
    • Export maps to CSV
    • Include error handling
  4. HTTP Client: Create an HTTP API wrapper

    • Use HTTPoison dependency
    • Parse JSON responses
    • Handle errors gracefully
    • Write integration tests

Great work! You're now ready to build Phoenix applications! 🚀