Session 07

Date: Wednesday, Feb 25
Entry ticket: None

We will use this synchronous session to transition into test-driven development (TDD). Writing code without tests is just guessing. Today, we will map the logic first, write the test second, and write the function last.

Triage (0:00 – 0:15)

Class begins with our standard stand-up. I will spend the first 15 or so minutes going over the most common friction points, along with some key trends I have been seeing across the project repositories, and demonstrate how I approach solving them.

Micro-Clinic (0:15 – 0:35)

For 20 minutes, we will focus on the absolute basics of pytest and the Red-Green-Refactor cycle. Testing is not magic; it is simply writing code that runs your code to ensure it behaves exactly as your manual calculations dictate.

We will start by creating a file named test_analysis.py (the test_ prefix is how pytest knows to look there).

The Failing Test (Red)

Before we write the function, we define the expected behavior. Let’s write a function to calculate Ligand Efficiency (LE) for our virtual screen. LE normalizes a docking score by the molecule’s size, defined as LE=ΔG/NheavyLE = \Delta G / N_{heavy}.

Before you type anything, you must calculate the expected result by hand. If our docking score is 9.0-9.0 kcal/mol and the molecule has 2525 heavy atoms, our LE is 9.3/25=0.36-9.3 / 25 = -0.36.

Because computers struggle with floating-point arithmetic (e.g., 0.1 + 0.2 = 0.30000000000000004), we cannot use a strict == sign for floats. We must use pytest.approx.

tests/test_analysis.py
import pytest
from analysis import calculate_le

def test_calculate_le_standard():
    score = -9.0
    heavy_atoms = 25
    expected_le = -0.36
    assert calculate_le(score, heavy_atoms) == pytest.approx(expected_le)

If we run pytest right now, it will fail because calculate_le does not exist. That is the point.

The Passing Code (Green)

Now, we write the absolute minimum code in analysis.py to make the test pass. We do not over-engineer it yet.

lab/analysis.py
def calculate_le(score: float, heavy_atoms: int) -> float:
    return score / heavy_atoms

The Cleanup (Refactor)

Once the test passes, we evaluate the system for fragility. What happens if our dataset contains an error and passes a molecule with 0 heavy atoms into our function? The script will crash with a ZeroDivisionError.

We can safely clean up our logic to handle this edge case, knowing our original test will catch us if we break the core math.

lab/analysis.py
def calculate_le(score: float, heavy_atoms: int) -> float:
    if heavy_atoms <= 0:
        raise ValueError("Heavy atoms must be a positive integer.")
    return score / heavy_atoms

We can now write a second test in test_analysis.py to intentionally pass 0 and verify that our custom ValueError is successfully raised, proving our safety rail works.

Deep Work (0:35 – 1:50)

For the next 55 minutes, teams will work on their Sprint 2 Prototype deliverables. Your objective is to write a single, robust Python function to accomplish a small task for your project, along with at least three pytests to verify its behavior. You will work closely with your immediate team members to decide what needs to be tested, but you may write the code individually or in pairs.

Focus on defining the function signature, expected inputs, and edge-case tests first. I will be heavily circulating the room to help you write your very first tests.

Last updated on