Files
codegraph/__tests__/pytest-fixture-receiver.test.ts
Colby Mchenry 92f141f04f fix(python): a pytest test's parameter is the class its fixture returns (#2228)
A receiver that is the test's own parameter takes the type its in-reach
fixture returns (`return Cls(…)` or a local assigned `Cls(…)`), so calls on
it resolve on that class, or on nothing when it is a library's; fixtures a
conftest.py star-imports or lists in pytest_plugins are in reach.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 01:35:48 +00:00

82 lines
2.5 KiB
TypeScript

/**
* A pytest test's parameter is what its fixture returns — `return Cls(…)`, or
* a local assigned `Cls(…)` — so a call on it is that class's method, and
* none of the project's when the class comes from outside: flask's
* `runner.invoke(cli, …)` on click's `CliRunner()` is not `FlaskCliRunner.invoke`.
* A fixture is in reach from its own module, a `conftest.py` above the test,
* or a module such a conftest star-imports or lists in `pytest_plugins`.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-pytest-fixture-'));
const files: Record<string, string> = {
'app/cli.py': `class AlembicGroup:
def invoke(self, ctx):
return ctx
`,
'app/models.py': `class Forum:
def save(self):
return self
`,
'tests/conftest.py': `from tests.fixtures.cli import * # noqa
from tests.fixtures.forum import * # noqa
`,
'tests/fixtures/cli.py': `import pytest
from click.testing import CliRunner
@pytest.fixture
def runner():
return CliRunner()
`,
'tests/fixtures/forum.py': `import pytest
from app.models import Forum
@pytest.fixture
def forum():
forum = Forum()
return forum
`,
'tests/test_cli.py': `def test_routes(runner):
result = runner.invoke(cli, ["routes"])
return result
def test_forum(forum):
forum.save()
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
const targetsFrom = (qualifiedName: string) => {
const ids = cg.getNodesInFile('tests/test_cli.py').filter((n) => n.qualifiedName === qualifiedName).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind !== 'contains').map((e) => cg.getNode(e.target)!).map((t) => `${t.filePath}:${t.qualifiedName}`);
};
describe('pytest fixture parameters', () => {
// Both fixtures live in modules `tests/conftest.py` star-imports.
it('are the class their fixture returns', () => {
expect(targetsFrom('test_routes')).not.toContain('app/cli.py:AlembicGroup::invoke');
expect(targetsFrom('test_forum')).toContain('app/models.py:Forum::save');
});
});