Initial commit: Requirements to Gherkin CLI Converter
This commit is contained in:
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests package for nl2gherkin."""
|
||||
116
tests/test_ambiguity.py
Normal file
116
tests/test_ambiguity.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Tests for the ambiguity detection module."""
|
||||
|
||||
import pytest
|
||||
from nl2gherkin.nlp.ambiguity import AmbiguityDetector, AmbiguityType
|
||||
|
||||
|
||||
class TestAmbiguityDetector:
|
||||
"""Test cases for AmbiguityDetector."""
|
||||
|
||||
@pytest.fixture
|
||||
def detector(self):
|
||||
"""Create a detector instance."""
|
||||
return AmbiguityDetector()
|
||||
|
||||
def test_pronoun_detection(self, detector):
|
||||
"""Test detection of ambiguous pronouns."""
|
||||
text = "When the user clicks it, the system should show the results"
|
||||
result = detector.detect(text)
|
||||
|
||||
assert len(result) >= 0
|
||||
|
||||
def test_vague_quantifier_some(self, detector):
|
||||
"""Test detection of 'some' quantifier."""
|
||||
text = "The system should handle some requests efficiently"
|
||||
result = detector.detect(text)
|
||||
|
||||
quantifier_types = [w.type for w in result]
|
||||
assert AmbiguityType.VAGUE_QUANTIFIER in quantifier_types
|
||||
|
||||
def test_vague_quantifier_many(self, detector):
|
||||
"""Test detection of 'many' quantifier."""
|
||||
text = "Many users should be able to login"
|
||||
result = detector.detect(text)
|
||||
|
||||
quantifier_types = [w.type for w in result]
|
||||
assert AmbiguityType.VAGUE_QUANTIFIER in quantifier_types
|
||||
|
||||
def test_vague_quantifier_few(self, detector):
|
||||
"""Test detection of 'few' quantifier."""
|
||||
text = "Only few options are available"
|
||||
result = detector.detect(text)
|
||||
|
||||
quantifier_types = [w.type for w in result]
|
||||
assert AmbiguityType.VAGUE_QUANTIFIER in quantifier_types
|
||||
|
||||
def test_temporal_ambiguity(self, detector):
|
||||
"""Test detection of temporal ambiguities."""
|
||||
text = "The task should be completed soon"
|
||||
result = detector.detect(text)
|
||||
|
||||
temporal_types = [w.type for w in result]
|
||||
assert AmbiguityType.TEMPORAL in temporal_types
|
||||
|
||||
def test_missing_condition(self, detector):
|
||||
"""Test detection of missing conditions."""
|
||||
text = "The user must login to access the dashboard"
|
||||
result = detector.detect(text)
|
||||
|
||||
condition_types = [w.type for w in result]
|
||||
assert AmbiguityType.MISSING_CONDITION in condition_types
|
||||
|
||||
def test_passive_voice_detection(self, detector):
|
||||
"""Test detection of passive voice."""
|
||||
text = "The file was created by the system"
|
||||
result = detector.detect(text)
|
||||
|
||||
voice_types = [w.type for w in result]
|
||||
assert AmbiguityType.PASSIVE_VOICE in voice_types
|
||||
|
||||
def test_clear_requirement_no_warnings(self, detector):
|
||||
"""Test that clear requirements have few warnings."""
|
||||
text = "When the user clicks the submit button, the form data is validated on the server"
|
||||
result = detector.detect(text)
|
||||
|
||||
assert len(result) <= 2
|
||||
|
||||
def test_warning_has_suggestion(self, detector):
|
||||
"""Test that warnings include suggestions."""
|
||||
text = "Some data should be processed"
|
||||
result = detector.detect(text)
|
||||
|
||||
if result:
|
||||
assert any(w.suggestion is not None for w in result)
|
||||
|
||||
def test_warning_severity_levels(self, detector):
|
||||
"""Test that warnings have severity levels."""
|
||||
text = "The report was generated"
|
||||
result = detector.detect(text)
|
||||
|
||||
for warning in result:
|
||||
assert warning.severity in ["low", "medium", "high"]
|
||||
|
||||
def test_to_dict_method(self, detector):
|
||||
"""Test that warnings can be converted to dict."""
|
||||
text = "It should work"
|
||||
result = detector.detect(text)
|
||||
|
||||
if result:
|
||||
warning_dict = result[0].to_dict()
|
||||
assert "type" in warning_dict
|
||||
assert "message" in warning_dict
|
||||
assert "suggestion" in warning_dict
|
||||
|
||||
def test_multiple_ambiguities(self, detector):
|
||||
"""Test detection of multiple ambiguities in one text."""
|
||||
text = "When it happens, some users might see many results eventually"
|
||||
result = detector.detect(text)
|
||||
|
||||
assert len(result) >= 2
|
||||
|
||||
def test_empty_text(self, detector):
|
||||
"""Test handling of empty text."""
|
||||
result = detector.detect("")
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 0
|
||||
119
tests/test_analyzer.py
Normal file
119
tests/test_analyzer.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Tests for the NLP analyzer module."""
|
||||
|
||||
import pytest
|
||||
from nl2gherkin.nlp.analyzer import ActionType, ActorType, NLPAnalyzer
|
||||
|
||||
|
||||
class TestNLPAnalyzer:
|
||||
"""Test cases for NLPAnalyzer."""
|
||||
|
||||
@pytest.fixture
|
||||
def analyzer(self):
|
||||
"""Create an analyzer instance."""
|
||||
return NLPAnalyzer()
|
||||
|
||||
def test_user_story_parsing(self, analyzer):
|
||||
"""Test parsing classic user story format."""
|
||||
text = "As a user, I want to login so that I can access my account"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.actor == "user"
|
||||
assert "login" in result.action.lower()
|
||||
assert result.benefit is not None
|
||||
|
||||
def test_user_story_with_admin(self, analyzer):
|
||||
"""Test parsing admin user story."""
|
||||
text = "As an administrator, I want to manage users so that I can control access"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.actor == "administrator"
|
||||
assert "manage" in result.action.lower() or result.action is not None
|
||||
assert result.benefit is not None
|
||||
|
||||
def test_simple_requirement(self, analyzer):
|
||||
"""Test parsing a simple requirement without user story format."""
|
||||
text = "The system should send an email notification"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.action is not None or "send" in result.raw_text.lower()
|
||||
|
||||
def test_action_extraction_create(self, analyzer):
|
||||
"""Test action extraction for create operations."""
|
||||
text = "As a user, I want to create a new document"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.action_type in [ActionType.CREATE, ActionType.UNKNOWN]
|
||||
|
||||
def test_action_extraction_read(self, analyzer):
|
||||
"""Test action extraction for read operations."""
|
||||
text = "As a user, I want to view my profile"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.action_type in [ActionType.READ, ActionType.UNKNOWN]
|
||||
|
||||
def test_action_extraction_delete(self, analyzer):
|
||||
"""Test action extraction for delete operations."""
|
||||
text = "As an admin, I want to delete the account"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.action_type in [ActionType.DELETE, ActionType.UNKNOWN]
|
||||
|
||||
def test_actor_type_user(self, analyzer):
|
||||
"""Test actor type detection for user."""
|
||||
text = "As a customer, I want to place an order"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.actor_type in [ActorType.USER, ActorType.UNKNOWN]
|
||||
|
||||
def test_actor_type_admin(self, analyzer):
|
||||
"""Test actor type detection for admin."""
|
||||
text = "As an administrator, I want to view logs"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.actor_type in [ActorType.ADMIN, ActorType.UNKNOWN]
|
||||
|
||||
def test_actor_type_system(self, analyzer):
|
||||
"""Test actor type detection for system."""
|
||||
text = "The system should validate the input"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.actor_type in [ActorType.SYSTEM, ActorType.UNKNOWN]
|
||||
|
||||
def test_condition_extraction(self, analyzer):
|
||||
"""Test condition extraction from requirements."""
|
||||
text = "When the user clicks submit, the form should be validated"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert result.condition is not None or "click" in result.raw_text.lower()
|
||||
|
||||
def test_variable_extraction(self, analyzer):
|
||||
"""Test variable/parameter extraction."""
|
||||
text = "As a user, I want to search for <product_name> so that I can find it"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
assert "product_name" in result.variables
|
||||
|
||||
def test_to_dict_method(self, analyzer):
|
||||
"""Test that to_dict returns a proper dictionary."""
|
||||
text = "As a user, I want to login"
|
||||
result = analyzer.analyze(text)
|
||||
|
||||
result_dict = result.to_dict()
|
||||
|
||||
assert isinstance(result_dict, dict)
|
||||
assert "actor" in result_dict
|
||||
assert "action" in result_dict
|
||||
assert "actor_type" in result_dict
|
||||
|
||||
def test_empty_input(self, analyzer):
|
||||
"""Test handling of empty input."""
|
||||
result = analyzer.analyze("")
|
||||
|
||||
assert result.raw_text == ""
|
||||
|
||||
def test_ambiguity_detection_call(self, analyzer):
|
||||
"""Test that ambiguity detection can be called."""
|
||||
text = "As a user, I want to do something"
|
||||
result = analyzer.analyze_ambiguity(text)
|
||||
|
||||
assert isinstance(result, list)
|
||||
127
tests/test_cli.py
Normal file
127
tests/test_cli.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""Tests for the CLI module."""
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from nl2gherkin.cli.commands import cli, convert, validate
|
||||
|
||||
|
||||
class TestCLI:
|
||||
"""Test cases for CLI commands."""
|
||||
|
||||
@pytest.fixture
|
||||
def runner(self):
|
||||
"""Create a CLI runner."""
|
||||
return CliRunner()
|
||||
|
||||
def test_cli_help(self, runner):
|
||||
"""Test CLI help command."""
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "convert" in result.output
|
||||
assert "interactive" in result.output
|
||||
assert "validate" in result.output
|
||||
|
||||
def test_convert_help(self, runner):
|
||||
"""Test convert command help."""
|
||||
result = runner.invoke(convert, ["--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "INPUT_FILE" in result.output
|
||||
assert "--output" in result.output
|
||||
assert "--framework" in result.output
|
||||
assert "--validate" in result.output
|
||||
|
||||
def test_convert_requires_input_file(self, runner):
|
||||
"""Test that convert requires an input file."""
|
||||
result = runner.invoke(convert, [])
|
||||
|
||||
assert result.exit_code != 0
|
||||
assert "INPUT_FILE" in result.output or "Missing argument" in result.output
|
||||
|
||||
def test_validate_help(self, runner):
|
||||
"""Test validate command help."""
|
||||
result = runner.invoke(validate, ["--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "GHERKIN_FILE" in result.output
|
||||
|
||||
def test_convert_with_sample_requirement(self, runner, tmp_path):
|
||||
"""Test converting a sample requirement file."""
|
||||
req_file = tmp_path / "requirements.txt"
|
||||
req_file.write_text("As a user, I want to login so that I can access my account")
|
||||
|
||||
result = runner.invoke(convert, [str(req_file), "--no-validate"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Feature:" in result.output
|
||||
assert "Given" in result.output
|
||||
assert "When" in result.output
|
||||
assert "Then" in result.output
|
||||
|
||||
def test_convert_with_output_file(self, runner, tmp_path):
|
||||
"""Test converting with output file option."""
|
||||
req_file = tmp_path / "requirements.txt"
|
||||
req_file.write_text("As a user, I want to search for products")
|
||||
|
||||
output_file = tmp_path / "output.feature"
|
||||
|
||||
result = runner.invoke(convert, [
|
||||
str(req_file),
|
||||
"--output", str(output_file),
|
||||
"--no-validate",
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert output_file.exists()
|
||||
|
||||
content = output_file.read_text()
|
||||
assert "Feature:" in content
|
||||
|
||||
def test_convert_with_different_frameworks(self, runner, tmp_path):
|
||||
"""Test converting with different BDD frameworks."""
|
||||
req_file = tmp_path / "requirements.txt"
|
||||
req_file.write_text("As a user, I want to login")
|
||||
|
||||
for framework in ["cucumber", "behave", "pytest-bdd"]:
|
||||
result = runner.invoke(convert, [
|
||||
str(req_file),
|
||||
"--framework", framework,
|
||||
"--no-validate",
|
||||
])
|
||||
|
||||
assert result.exit_code == 0, f"Failed for framework: {framework}"
|
||||
|
||||
def test_validate_valid_gherkin(self, runner, tmp_path):
|
||||
"""Test validating a valid Gherkin file."""
|
||||
gherkin_file = tmp_path / "test.feature"
|
||||
gherkin_file.write_text("""Feature: Test
|
||||
Scenario: A test scenario
|
||||
Given a setup
|
||||
When an action occurs
|
||||
Then an expected result
|
||||
""")
|
||||
|
||||
result = runner.invoke(validate, [str(gherkin_file)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_ambiguity_check_flag(self, runner, tmp_path):
|
||||
"""Test that ambiguity check flag produces warnings."""
|
||||
req_file = tmp_path / "requirements.txt"
|
||||
req_file.write_text("As a user, I want to do something with some data")
|
||||
|
||||
result = runner.invoke(convert, [
|
||||
str(req_file),
|
||||
"--ambiguity-check",
|
||||
"--no-validate",
|
||||
])
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_interactive_command_exists(self, runner):
|
||||
"""Test that interactive command exists."""
|
||||
result = runner.invoke(cli, ["interactive", "--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "--framework" in result.output
|
||||
178
tests/test_generator.py
Normal file
178
tests/test_generator.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""Tests for the Gherkin generator module."""
|
||||
|
||||
import pytest
|
||||
from nl2gherkin.gherkin.generator import (
|
||||
GherkinFeature,
|
||||
GherkinGenerator,
|
||||
GherkinScenario,
|
||||
ScenarioType,
|
||||
)
|
||||
from nl2gherkin.nlp.analyzer import ActionType, ActorType, RequirementAnalysis
|
||||
|
||||
|
||||
class TestGherkinGenerator:
|
||||
"""Test cases for GherkinGenerator."""
|
||||
|
||||
@pytest.fixture
|
||||
def generator(self):
|
||||
"""Create a generator instance."""
|
||||
return GherkinGenerator()
|
||||
|
||||
@pytest.fixture
|
||||
def analysis(self):
|
||||
"""Create a sample analysis."""
|
||||
return RequirementAnalysis(
|
||||
raw_text="As a user, I want to login",
|
||||
actor="user",
|
||||
actor_type=ActorType.USER,
|
||||
action="login",
|
||||
action_type=ActionType.LOGIN,
|
||||
target="account",
|
||||
)
|
||||
|
||||
def test_generate_simple_scenario(self, generator, analysis):
|
||||
"""Test generating a simple scenario."""
|
||||
result = generator.generate(analysis)
|
||||
|
||||
assert "Feature:" in result
|
||||
assert "Scenario:" in result
|
||||
assert "Given" in result
|
||||
assert "When" in result
|
||||
assert "Then" in result
|
||||
|
||||
def test_feature_name_from_actor_and_action(self, generator, analysis):
|
||||
"""Test feature name is derived from actor and action."""
|
||||
result = generator.generate(analysis)
|
||||
|
||||
assert "user login" in result.lower()
|
||||
|
||||
def test_scenario_name_generation(self, generator, analysis):
|
||||
"""Test scenario name is properly generated."""
|
||||
scenario = generator.generate_scenario(analysis)
|
||||
|
||||
assert scenario.name is not None
|
||||
assert len(scenario.name) > 0
|
||||
|
||||
def test_scenario_type_regular(self, generator, analysis):
|
||||
"""Test regular scenario type."""
|
||||
scenario = generator.generate_scenario(analysis)
|
||||
|
||||
assert scenario.scenario_type == ScenarioType.SCENARIO
|
||||
|
||||
def test_scenario_type_outline_with_variables(self, generator):
|
||||
"""Test scenario outline type with variables."""
|
||||
analysis = RequirementAnalysis(
|
||||
raw_text="As a user, I want to search for <product>",
|
||||
actor="user",
|
||||
action="search",
|
||||
variables={"product": "string"},
|
||||
examples=["product"],
|
||||
)
|
||||
|
||||
scenario = generator.generate_scenario(analysis)
|
||||
|
||||
assert scenario.scenario_type == ScenarioType.SCENARIO_OUTLINE
|
||||
|
||||
def test_examples_table_generation(self, generator):
|
||||
"""Test Examples table is generated for outlines."""
|
||||
analysis = RequirementAnalysis(
|
||||
raw_text="As a user, I want to search for <product>",
|
||||
actor="user",
|
||||
action="search",
|
||||
variables={"product": "string"},
|
||||
examples=["product"],
|
||||
)
|
||||
|
||||
result = generator.generate(analysis)
|
||||
|
||||
assert "Examples:" in result
|
||||
assert "| product |" in result
|
||||
|
||||
def test_steps_have_correct_keywords(self, generator, analysis):
|
||||
"""Test that steps have correct Gherkin keywords."""
|
||||
scenario = generator.generate_scenario(analysis)
|
||||
|
||||
keywords = [step.keyword for step in scenario.steps]
|
||||
assert "Given" in keywords
|
||||
assert "When" in keywords
|
||||
assert "Then" in keywords
|
||||
|
||||
def test_benefit_in_description(self, generator):
|
||||
"""Test that benefit is included in description."""
|
||||
analysis = RequirementAnalysis(
|
||||
raw_text="As a user, I want to login so that I can access my account",
|
||||
actor="user",
|
||||
action="login",
|
||||
benefit="I can access my account",
|
||||
)
|
||||
|
||||
result = generator.generate(analysis)
|
||||
|
||||
assert "can access my account" in result
|
||||
|
||||
def test_condition_as_given(self, generator):
|
||||
"""Test that conditions are converted to Given steps."""
|
||||
analysis = RequirementAnalysis(
|
||||
raw_text="When the user is logged in",
|
||||
actor="user",
|
||||
action="view",
|
||||
target="dashboard",
|
||||
condition="the user is logged in",
|
||||
)
|
||||
|
||||
scenario = generator.generate_scenario(analysis)
|
||||
|
||||
given_steps = [s for s in scenario.steps if s.keyword == "Given"]
|
||||
assert len(given_steps) >= 1
|
||||
|
||||
def test_read_action_display_then_step(self, generator):
|
||||
"""Test that read actions use 'displayed' in Then step."""
|
||||
analysis = RequirementAnalysis(
|
||||
raw_text="As a user, I want to view my profile",
|
||||
actor="user",
|
||||
action="view",
|
||||
action_type=ActionType.READ,
|
||||
target="profile",
|
||||
)
|
||||
|
||||
scenario = generator.generate_scenario(analysis)
|
||||
|
||||
then_steps = [s for s in scenario.steps if s.keyword == "Then"]
|
||||
assert len(then_steps) > 0
|
||||
assert "displayed" in then_steps[0].text.lower()
|
||||
|
||||
def test_empty_analysis(self, generator):
|
||||
"""Test handling of empty analysis."""
|
||||
analysis = RequirementAnalysis(raw_text="")
|
||||
|
||||
result = generator.generate(analysis)
|
||||
|
||||
assert "Feature:" in result
|
||||
|
||||
def test_render_feature_structure(self, generator, analysis):
|
||||
"""Test that rendered feature has proper structure."""
|
||||
feature = generator._create_feature(analysis)
|
||||
|
||||
assert isinstance(feature, GherkinFeature)
|
||||
assert feature.name is not None
|
||||
assert len(feature.scenarios) == 1
|
||||
assert isinstance(feature.scenarios[0], GherkinScenario)
|
||||
|
||||
def test_multiple_scenarios(self, generator):
|
||||
"""Test generating multiple scenarios."""
|
||||
analysis1 = RequirementAnalysis(
|
||||
raw_text="As a user, I want to login",
|
||||
actor="user",
|
||||
action="login",
|
||||
)
|
||||
analysis2 = RequirementAnalysis(
|
||||
raw_text="As a user, I want to logout",
|
||||
actor="user",
|
||||
action="logout",
|
||||
)
|
||||
|
||||
feature1 = generator._create_feature(analysis1)
|
||||
feature2 = generator._create_feature(analysis2)
|
||||
|
||||
assert len(feature1.scenarios) == 1
|
||||
assert len(feature2.scenarios) == 1
|
||||
Reference in New Issue
Block a user