@@ -194,6 +194,7 @@ def test_scaffold_help_lists_new_targets():
194194 assert "gitlab" in result .stdout
195195 assert "argocd" in result .stdout
196196 assert "sre" in result .stdout
197+ assert "unittest" in result .stdout
197198
198199def test_scaffold_gha_via_cli ():
199200 """Regression: `python -m cli.devopsos scaffold gha` must not raise argparse error."""
@@ -596,11 +597,11 @@ def test_process_first_specific_section_no_usage_footer():
596597# -- scaffold arg pass-through (cli.devopsos vs cli.scaffold_*) ------------
597598
598599def test_scaffold_help_shows_all_subcommands ():
599- """`devopsos scaffold --help` lists all 7 scaffold subcommands."""
600+ """`devopsos scaffold --help` lists all 8 scaffold subcommands."""
600601 result = _run (["-m" , "cli.devopsos" , "scaffold" , "--help" ])
601602 assert result .returncode == 0
602603 text = _strip_ansi (result .stdout )
603- for target in ("gha" , "jenkins" , "gitlab" , "argocd" , "sre" , "devcontainer" , "cicd" ):
604+ for target in ("gha" , "jenkins" , "gitlab" , "argocd" , "sre" , "devcontainer" , "cicd" , "unittest" ):
604605 assert target in text , f"scaffold --help should list the '{ target } ' subcommand"
605606
606607
@@ -759,11 +760,219 @@ def test_scaffold_cicd_jenkins_only():
759760 )
760761
761762
763+ # ── scaffold unittest (new in v0.4.0) ────────────────────────────────────────
764+
765+ def test_scaffold_unittest_help_shows_native_options ():
766+ """`devopsos scaffold unittest --help` shows all unittest-specific options."""
767+ result = _run (["-m" , "cli.devopsos" , "scaffold" , "unittest" , "--help" ])
768+ assert result .returncode == 0
769+ text = _strip_ansi (result .stdout )
770+ for option in ("--name" , "--languages" , "--framework" , "--coverage" , "--output-dir" ):
771+ assert option in text , f"scaffold unittest --help should show '{ option } '"
772+ # Supported languages / frameworks must be mentioned in the help text
773+ assert "python" in text .lower ()
774+ assert "javascript" in text .lower ()
775+ assert "typescript" in text .lower ()
776+ assert "go" in text .lower ()
777+ assert "jest" in text .lower ()
778+
779+
780+ def test_scaffold_unittest_via_cli_python ():
781+ """`devopsos scaffold unittest --languages python` generates pytest.ini and conftest.py."""
782+ with tempfile .TemporaryDirectory () as tmp :
783+ result = subprocess .run (
784+ [
785+ sys .executable , "-m" , "cli.devopsos" ,
786+ "scaffold" , "unittest" ,
787+ "--name" , "my-api" ,
788+ "--languages" , "python" ,
789+ "--output-dir" , tmp ,
790+ ],
791+ capture_output = True , text = True ,
792+ cwd = os .path .dirname (os .path .dirname (__file__ )),
793+ )
794+ assert result .returncode == 0 , result .stderr + result .stdout
795+ assert "error: unrecognized arguments" not in result .stderr
796+ assert (Path (tmp ) / "pytest.ini" ).is_file (), "pytest.ini should be created"
797+ assert (Path (tmp ) / "conftest.py" ).is_file (), "conftest.py should be created"
798+ assert (Path (tmp ) / "tests" / "test_sample.py" ).is_file (), "test_sample.py should be created"
799+ assert (Path (tmp ) / "tests" / "__init__.py" ).is_file (), "tests/__init__.py should be created"
800+ # pytest.ini must reference the project name
801+ ini_content = (Path (tmp ) / "pytest.ini" ).read_text ()
802+ assert "testpaths" in ini_content
803+ assert "--cov=" in ini_content # coverage enabled by default
804+
805+
806+ def test_scaffold_unittest_via_cli_javascript_jest ():
807+ """`devopsos scaffold unittest --languages javascript --framework jest` generates jest.config.js."""
808+ with tempfile .TemporaryDirectory () as tmp :
809+ result = subprocess .run (
810+ [
811+ sys .executable , "-m" , "cli.devopsos" ,
812+ "scaffold" , "unittest" ,
813+ "--name" , "my-lib" ,
814+ "--languages" , "javascript" ,
815+ "--framework" , "jest" ,
816+ "--output-dir" , tmp ,
817+ ],
818+ capture_output = True , text = True ,
819+ cwd = os .path .dirname (os .path .dirname (__file__ )),
820+ )
821+ assert result .returncode == 0 , result .stderr + result .stdout
822+ assert (Path (tmp ) / "jest.config.js" ).is_file (), "jest.config.js should be created"
823+ assert (Path (tmp ) / "tests" / "sample.test.js" ).is_file (), "sample.test.js should be created"
824+ jest_content = (Path (tmp ) / "jest.config.js" ).read_text ()
825+ assert "testEnvironment" in jest_content
826+ assert "collectCoverage" in jest_content # coverage enabled by default
827+
828+
829+ def test_scaffold_unittest_via_cli_typescript_vitest ():
830+ """`devopsos scaffold unittest --languages typescript --framework vitest` creates vitest.config.js."""
831+ with tempfile .TemporaryDirectory () as tmp :
832+ result = subprocess .run (
833+ [
834+ sys .executable , "-m" , "cli.devopsos" ,
835+ "scaffold" , "unittest" ,
836+ "--name" , "my-ui" ,
837+ "--languages" , "typescript" ,
838+ "--framework" , "vitest" ,
839+ "--output-dir" , tmp ,
840+ ],
841+ capture_output = True , text = True ,
842+ cwd = os .path .dirname (os .path .dirname (__file__ )),
843+ )
844+ assert result .returncode == 0 , result .stderr + result .stdout
845+ assert (Path (tmp ) / "vitest.config.js" ).is_file (), "vitest.config.js should be created"
846+ assert (Path (tmp ) / "tests" / "sample.test.ts" ).is_file (), "sample.test.ts should be created"
847+ vitest_content = (Path (tmp ) / "vitest.config.js" ).read_text ()
848+ assert "defineConfig" in vitest_content
849+ assert "vitest/config" in vitest_content
850+
851+
852+ def test_scaffold_unittest_via_cli_javascript_mocha ():
853+ """`devopsos scaffold unittest --languages javascript --framework mocha` generates .mocharc.js."""
854+ with tempfile .TemporaryDirectory () as tmp :
855+ result = subprocess .run (
856+ [
857+ sys .executable , "-m" , "cli.devopsos" ,
858+ "scaffold" , "unittest" ,
859+ "--name" , "my-service" ,
860+ "--languages" , "javascript" ,
861+ "--framework" , "mocha" ,
862+ "--output-dir" , tmp ,
863+ ],
864+ capture_output = True , text = True ,
865+ cwd = os .path .dirname (os .path .dirname (__file__ )),
866+ )
867+ assert result .returncode == 0 , result .stderr + result .stdout
868+ assert (Path (tmp ) / ".mocharc.js" ).is_file (), ".mocharc.js should be created"
869+ assert (Path (tmp ) / "tests" / "sample.test.js" ).is_file (), "sample.test.js should be created"
870+ mocha_content = (Path (tmp ) / ".mocharc.js" ).read_text ()
871+ assert "spec" in mocha_content
872+ assert "tests/**" in mocha_content
873+
874+
875+ def test_scaffold_unittest_via_cli_go ():
876+ """`devopsos scaffold unittest --languages go` generates a Go test file and Makefile."""
877+ with tempfile .TemporaryDirectory () as tmp :
878+ result = subprocess .run (
879+ [
880+ sys .executable , "-m" , "cli.devopsos" ,
881+ "scaffold" , "unittest" ,
882+ "--name" , "my-server" ,
883+ "--languages" , "go" ,
884+ "--output-dir" , tmp ,
885+ ],
886+ capture_output = True , text = True ,
887+ cwd = os .path .dirname (os .path .dirname (__file__ )),
888+ )
889+ assert result .returncode == 0 , result .stderr + result .stdout
890+ assert (Path (tmp ) / "my_server_test.go" ).is_file (), "my_server_test.go should be created"
891+ assert (Path (tmp ) / "Makefile.test" ).is_file (), "Makefile.test should be created"
892+ go_content = (Path (tmp ) / "my_server_test.go" ).read_text ()
893+ assert '"testing"' in go_content
894+ assert "TestTableDriven" in go_content
895+ makefile_content = (Path (tmp ) / "Makefile.test" ).read_text ()
896+ assert "go test" in makefile_content
897+ assert "test-cov:" in makefile_content
898+
899+
900+ def test_scaffold_unittest_via_cli_multi_language ():
901+ """`devopsos scaffold unittest --languages python,javascript,go` generates files for all three."""
902+ with tempfile .TemporaryDirectory () as tmp :
903+ result = subprocess .run (
904+ [
905+ sys .executable , "-m" , "cli.devopsos" ,
906+ "scaffold" , "unittest" ,
907+ "--name" , "full-stack" ,
908+ "--languages" , "python,javascript,go" ,
909+ "--output-dir" , tmp ,
910+ ],
911+ capture_output = True , text = True ,
912+ cwd = os .path .dirname (os .path .dirname (__file__ )),
913+ )
914+ assert result .returncode == 0 , result .stderr + result .stdout
915+ # Python files
916+ assert (Path (tmp ) / "pytest.ini" ).is_file (), "pytest.ini should be created"
917+ assert (Path (tmp ) / "tests" / "test_sample.py" ).is_file (), "test_sample.py should be created"
918+ # JavaScript files (default: jest)
919+ assert (Path (tmp ) / "jest.config.js" ).is_file (), "jest.config.js should be created"
920+ # Go files
921+ assert (Path (tmp ) / "full_stack_test.go" ).is_file (), "full_stack_test.go should be created"
922+ assert (Path (tmp ) / "Makefile.test" ).is_file (), "Makefile.test should be created"
923+
924+
925+ def test_scaffold_unittest_no_coverage_flag ():
926+ """`--no-coverage` removes coverage configuration from generated files."""
927+ with tempfile .TemporaryDirectory () as tmp :
928+ result = subprocess .run (
929+ [
930+ sys .executable , "-m" , "cli.devopsos" ,
931+ "scaffold" , "unittest" ,
932+ "--name" , "lean-app" ,
933+ "--languages" , "python" ,
934+ "--no-coverage" ,
935+ "--output-dir" , tmp ,
936+ ],
937+ capture_output = True , text = True ,
938+ cwd = os .path .dirname (os .path .dirname (__file__ )),
939+ )
940+ assert result .returncode == 0 , result .stderr + result .stdout
941+ ini_content = (Path (tmp ) / "pytest.ini" ).read_text ()
942+ assert "--cov=" not in ini_content , "pytest.ini should not contain --cov= when --no-coverage is set"
943+
944+
945+ def test_scaffold_unittest_via_cli_matches_direct_module_output ():
946+ """Output from `devopsos scaffold unittest` equals `python -m cli.scaffold_unittest` (same defaults)."""
947+ with tempfile .TemporaryDirectory () as tmp1 , tempfile .TemporaryDirectory () as tmp2 :
948+ common_args = ["--name" , "test-app" , "--languages" , "python" ]
949+
950+ result_unified = subprocess .run (
951+ [sys .executable , "-m" , "cli.devopsos" , "scaffold" , "unittest" ]
952+ + common_args + ["--output-dir" , tmp1 ],
953+ capture_output = True , text = True ,
954+ cwd = os .path .dirname (os .path .dirname (__file__ )),
955+ )
956+ result_direct = subprocess .run (
957+ [sys .executable , "-m" , "cli.scaffold_unittest" ]
958+ + common_args + ["--output-dir" , tmp2 ],
959+ capture_output = True , text = True ,
960+ cwd = os .path .dirname (os .path .dirname (__file__ )),
961+ )
962+
963+ assert result_unified .returncode == 0 , result_unified .stderr
964+ assert result_direct .returncode == 0 , result_direct .stderr
965+ # Both must produce identical pytest.ini content
966+ assert (Path (tmp1 ) / "pytest.ini" ).read_text () == (Path (tmp2 ) / "pytest.ini" ).read_text (), (
967+ "Unified CLI and direct module should produce identical pytest.ini"
968+ )
969+
970+
762971# ── graceful exit (no opts) ──────────────────────────────────────────────────
763972
764973def test_scaffold_no_opts_shows_help ():
765974 """Each scaffold subcommand shows usage help (not an error) when invoked with no options."""
766- targets = ("gha" , "jenkins" , "gitlab" , "argocd" , "sre" , "devcontainer" , "cicd" )
975+ targets = ("gha" , "jenkins" , "gitlab" , "argocd" , "sre" , "devcontainer" , "cicd" , "unittest" )
767976 for target in targets :
768977 result = subprocess .run (
769978 [sys .executable , "-m" , "cli.devopsos" , "scaffold" , target ],
0 commit comments