diff --git a/cmd/compose.go b/cmd/compose.go new file mode 100644 index 00000000..94a65652 --- /dev/null +++ b/cmd/compose.go @@ -0,0 +1,70 @@ +/* +Copyright 2025 API Testing Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" +) + +type composeOptions struct { + runOption + projectName string +} + +func createComposeRun() *cobra.Command { + cmd := &cobra.Command{ + Use: "compose", + } + + cmd.AddCommand(createComposeRunUp()) + return cmd +} + +type composeMsgWriter struct { +} + +func (w *composeMsgWriter) Write(p []byte) (n int, err error) { + fmt.Println(`{ "type": "info", "message": "` + strings.TrimSpace(string(p)) + `" }`) + return +} + +func (o *composeOptions) preRunE(cmd *cobra.Command, args []string) error { + return o.runOption.preRunE(cmd, nil) +} + +func (o *composeOptions) runE(cmd *cobra.Command, args []string) error { + return o.runOption.runE(cmd, args) +} + +func createComposeRunUp() *cobra.Command { + opt := &composeOptions{ + runOption: *newDefaultRunOption(&composeMsgWriter{}), + } + c := &cobra.Command{ + Use: "up", + PreRunE: opt.preRunE, + RunE: opt.runE, + } + c.SetOut(&composeMsgWriter{}) + + c.Flags().StringVarP(&opt.projectName, "project-name", "", "", "Specify an alternate project name") + opt.runOption.addFlags(c.Flags()) + return c +} diff --git a/cmd/root.go b/cmd/root.go index 58e595e6..7d6d30af 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -39,7 +39,8 @@ func NewRootCmd(execer fakeruntime.Execer, httpServer server.HTTPServer) (c *cob createRunCommand(), createSampleCmd(), createMockComposeCmd(), createServerCmd(execer, httpServer), createJSONSchemaCmd(), createServiceCommand(execer), createFunctionCmd(), createConvertCommand(), - createMockCmd(), createExtensionCommand(downloader.NewStoreDownloader())) + createMockCmd(), createExtensionCommand(downloader.NewStoreDownloader()), + createComposeRun()) return } diff --git a/cmd/run.go b/cmd/run.go index bce1da18..32d501f5 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -79,10 +79,13 @@ var ( runLogger = logging.DefaultLogger(logging.LogLevelInfo).WithName("run") ) -func newDefaultRunOption() *runOption { +func newDefaultRunOption(output io.Writer) *runOption { + if output == nil { + output = os.Stdout + } return &runOption{ reporter: runner.NewMemoryTestReporter(nil, ""), - reportWriter: runner.NewResultWriter(os.Stdout), + reportWriter: runner.NewResultWriter(output), loader: testing.NewFileLoader(), githubReportOption: &runner.GithubPRCommentOption{}, } @@ -97,7 +100,7 @@ func newDiscardRunOption() *runOption { // createRunCommand returns the run command func createRunCommand() (cmd *cobra.Command) { - opt := newDefaultRunOption() + opt := newDefaultRunOption(nil) cmd = &cobra.Command{ Use: "run", Aliases: []string{"r"}, @@ -111,29 +114,33 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`, // set flags flags := cmd.Flags() - flags.StringVarP(&opt.pattern, "pattern", "p", "test-suite-*.yaml", - "The file pattern which try to execute the test cases. Brace expansion is supported, such as: test-suite-{1,2}.yaml") - flags.StringVarP(&opt.level, "level", "l", "info", "Set the output log level") - flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration") - flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request") - flags.BoolVarP(&opt.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error") - flags.StringArrayVarP(&opt.caseFilter, "case-filter", "", nil, "The filter of the test case") - flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, json, discard, std, prometheus, http, grpc") - flags.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report") - flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output") - flags.StringVarP(&opt.reportTemplate, "report-template", "", "", "The template used to render the report") - flags.StringVarP(&opt.reportDest, "report-dest", "", "", "The server url where you want to send the report") - flags.StringVarP(&opt.swaggerURL, "swagger-url", "", "", "The URL of swagger") - flags.Int64VarP(&opt.thread, "thread", "", 1, "Threads of the execution") - flags.Int32VarP(&opt.qps, "qps", "", 5, "QPS") - flags.Int32VarP(&opt.burst, "burst", "", 5, "burst") - flags.StringVarP(&opt.monitorDocker, "monitor-docker", "", "", "The docker container name to monitor") + opt.addFlags(flags) addGitHubReportFlags(flags, opt.githubReportOption) return } const caseFilter = "case-filter" +func (o *runOption) addFlags(flags *pflag.FlagSet) { + flags.StringVarP(&o.pattern, "pattern", "p", "test-suite-*.yaml", + "The file pattern which try to execute the test cases. Brace expansion is supported, such as: test-suite-{1,2}.yaml") + flags.StringVarP(&o.level, "level", "l", "info", "Set the output log level") + flags.DurationVarP(&o.duration, "duration", "", 0, "Running duration") + flags.DurationVarP(&o.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request") + flags.BoolVarP(&o.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error") + flags.StringArrayVarP(&o.caseFilter, "case-filter", "", nil, "The filter of the test case") + flags.StringVarP(&o.report, "report", "", "", "The type of target report. Supported: markdown, md, html, json, discard, std, prometheus, http, grpc") + flags.StringVarP(&o.reportFile, "report-file", "", "", "The file path of the report") + flags.BoolVarP(&o.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output") + flags.StringVarP(&o.reportTemplate, "report-template", "", "", "The template used to render the report") + flags.StringVarP(&o.reportDest, "report-dest", "", "", "The server url where you want to send the report") + flags.StringVarP(&o.swaggerURL, "swagger-url", "", "", "The URL of swagger") + flags.Int64VarP(&o.thread, "thread", "", 1, "Threads of the execution") + flags.Int32VarP(&o.qps, "qps", "", 5, "QPS") + flags.Int32VarP(&o.burst, "burst", "", 5, "burst") + flags.StringVarP(&o.monitorDocker, "monitor-docker", "", "", "The docker container name to monitor") +} + func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() if ctx == nil { @@ -148,6 +155,9 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) { return } + defer func() { + _ = reportFile.Close() + }() writer = io.MultiWriter(writer, reportFile) } @@ -354,7 +364,7 @@ func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]inter suiteRunner := runner.GetTestSuiteRunner(testSuite) suiteRunner.WithTestReporter(o.reporter) suiteRunner.WithSecure(testSuite.Spec.Secure) - suiteRunner.WithOutputWriter(os.Stdout) + suiteRunner.WithOutputWriter(o.reportWriter.GetWriter()) suiteRunner.WithWriteLevel(o.level) suiteRunner.WithSuite(testSuite) var caseFilterObj interface{} diff --git a/docs/site/content/zh/latest/tasks/code-generator.md b/docs/site/content/zh/latest/tasks/code-generator.md index 216f8dfb..ac576392 100644 --- a/docs/site/content/zh/latest/tasks/code-generator.md +++ b/docs/site/content/zh/latest/tasks/code-generator.md @@ -1,6 +1,6 @@ +++ title = "代码生成" -weight = 103 +weight = 104 +++ `atest` 支持把测试用例生成多种开发语言的代码: diff --git a/docs/site/content/zh/latest/tasks/e2e.md b/docs/site/content/zh/latest/tasks/e2e.md new file mode 100644 index 00000000..ffd2d3bc --- /dev/null +++ b/docs/site/content/zh/latest/tasks/e2e.md @@ -0,0 +1,58 @@ ++++ +title = "End-to-End" +weight = 103 ++++ + +`atest` 非常适合针对(HTTP)接口做 E2E(端到端)测试,E2E 测试可以确保后端接口在完整的环境中持续地保持正确运行。下面采用 Docker compose 给出一个使用事例: + +```yaml +version: '3.1' +services: + testing: + image: ghcr.io/linuxsuren/api-testing:latest + environment: + SERVER: http://server:8080 + volumes: + - ./testsuite.yaml:/work/testsuite.yaml + command: atest run -p /work/testsuite.yaml + depends_on: + server: + condition: service_healthy + links: + - server + server: + image: ghcr.io/devops-ws/learn-springboot:master + healthcheck: + test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/8080"] + interval: 3s + timeout: 60s + retries: 10 + start_period: 3s +``` + +从 Docker compose `v2.36.0` 开始,可以采用如下简化的写法: + +```yaml +services: + testing: + scale: 0 + provider: + type: atest + options: + pattern: testsuite.yaml + environment: + SERVER: http://server:8080 + depends_on: + server: + condition: service_healthy + links: + - server + server: + image: ghcr.io/devops-ws/learn-springboot:master + healthcheck: + test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/8080"] + interval: 3s + timeout: 60s + retries: 10 + start_period: 3s +``` diff --git a/docs/site/content/zh/latest/tasks/testsuite.yaml b/docs/site/content/zh/latest/tasks/testsuite.yaml new file mode 100644 index 00000000..fb4d5c81 --- /dev/null +++ b/docs/site/content/zh/latest/tasks/testsuite.yaml @@ -0,0 +1,9 @@ +#!api-testing +# yaml-language-server: $schema=https://linuxsuren.github.io/api-testing/api-testing-schema.json +name: SpringBoot +api: | + {{default "http://localhost:8080" (env "SERVER")}} +items: +- name: health + request: + api: /health diff --git a/main.go b/main.go index fed4097e..51fa223f 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,10 @@ package main import ( _ "embed" - "github.com/linuxsuren/api-testing/pkg/version" "os" + "github.com/linuxsuren/api-testing/pkg/version" + "github.com/linuxsuren/api-testing/cmd" "github.com/linuxsuren/api-testing/pkg/server" exec "github.com/linuxsuren/go-fake-runtime" diff --git a/pkg/logging/log.go b/pkg/logging/log.go index fc62f12c..acd967e5 100644 --- a/pkg/logging/log.go +++ b/pkg/logging/log.go @@ -118,8 +118,12 @@ func DefaultLogger(level LogLevel) Logger { // contain only letters, digits, and hyphens (see the package documentation for // more information). func (l Logger) WithName(name string) Logger { + return l.WithNameAndWriter(name, os.Stdout) +} + +func (l Logger) WithNameAndWriter(name string, writer io.Writer) Logger { logLevel := l.logging.Level[APITestingLogComponent(name)] - logger := initZapLogger(os.Stdout, l.logging, logLevel) + logger := initZapLogger(writer, l.logging, logLevel) return Logger{ Logger: zapr.NewLogger(logger).WithName(name), diff --git a/pkg/runner/writer.go b/pkg/runner/writer.go index 2f31b459..91cca952 100644 --- a/pkg/runner/writer.go +++ b/pkg/runner/writer.go @@ -16,11 +16,16 @@ limitations under the License. package runner -import "github.com/linuxsuren/api-testing/pkg/apispec" +import ( + "io" + + "github.com/linuxsuren/api-testing/pkg/apispec" +) // ReportResultWriter is the interface of the report writer type ReportResultWriter interface { Output([]ReportResult) error WithAPICoverage(apiCoverage apispec.APICoverage) ReportResultWriter WithResourceUsage([]ResourceUsage) ReportResultWriter + GetWriter() io.Writer } diff --git a/pkg/runner/writer_github_pr_comment.go b/pkg/runner/writer_github_pr_comment.go index 8eccc511..d71b3633 100644 --- a/pkg/runner/writer_github_pr_comment.go +++ b/pkg/runner/writer_github_pr_comment.go @@ -1,5 +1,5 @@ /* -Copyright 2023 API Testing Authors. +Copyright 2023-2025 API Testing Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -194,6 +194,10 @@ func (w *githubPRCommentWriter) WithResourceUsage([]ResourceUsage) ReportResultW return w } +func (w *githubPRCommentWriter) GetWriter() io.Writer { + return os.Stdout +} + func unmarshalResponseBody(resp *http.Response, expectedCode int, obj interface{}) (err error) { if resp.StatusCode != expectedCode { err = fmt.Errorf("expect status code: %d, but %d", expectedCode, resp.StatusCode) diff --git a/pkg/runner/writer_grpc.go b/pkg/runner/writer_grpc.go index b38e0b9e..8c53b454 100644 --- a/pkg/runner/writer_grpc.go +++ b/pkg/runner/writer_grpc.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "log" "github.com/linuxsuren/api-testing/pkg/apispec" @@ -118,3 +119,7 @@ func (w *grpcResultWriter) WithAPICoverage(apiConverage apispec.APICoverage) Rep func (w *grpcResultWriter) WithResourceUsage([]ResourceUsage) ReportResultWriter { return w } + +func (w *grpcResultWriter) GetWriter() io.Writer { + return nil +} diff --git a/pkg/runner/writer_html.go b/pkg/runner/writer_html.go index 870baec9..97c3369d 100644 --- a/pkg/runner/writer_html.go +++ b/pkg/runner/writer_html.go @@ -49,5 +49,9 @@ func (w *htmlResultWriter) WithResourceUsage([]ResourceUsage) ReportResultWriter return w } +func (w *htmlResultWriter) GetWriter() io.Writer { + return nil +} + //go:embed data/html.html var htmlReport string diff --git a/pkg/runner/writer_http.go b/pkg/runner/writer_http.go index 138dbad3..72108775 100644 --- a/pkg/runner/writer_http.go +++ b/pkg/runner/writer_http.go @@ -148,3 +148,7 @@ func (w *httpResultWriter) WithAPICoverage(apiConverage apispec.APICoverage) Rep func (w *httpResultWriter) WithResourceUsage([]ResourceUsage) ReportResultWriter { return w } + +func (w *httpResultWriter) GetWriter() io.Writer { + return nil +} diff --git a/pkg/runner/writer_json.go b/pkg/runner/writer_json.go index a874baf0..0b353968 100644 --- a/pkg/runner/writer_json.go +++ b/pkg/runner/writer_json.go @@ -51,3 +51,7 @@ func (w *jsonResultWriter) WithAPICoverage(apiConverage apispec.APICoverage) Rep func (w *jsonResultWriter) WithResourceUsage([]ResourceUsage) ReportResultWriter { return w } + +func (w *jsonResultWriter) GetWriter() io.Writer { + return w.writer +} diff --git a/pkg/runner/writer_markdown.go b/pkg/runner/writer_markdown.go index 0a8af5ba..76349316 100644 --- a/pkg/runner/writer_markdown.go +++ b/pkg/runner/writer_markdown.go @@ -69,6 +69,10 @@ func (w *markdownResultWriter) WithResourceUsage(resurceUage []ResourceUsage) Re return w } +func (w *markdownResultWriter) GetWriter() io.Writer { + return w.writer +} + type markdownReport struct { Total int Error int diff --git a/pkg/runner/writer_pdf.go b/pkg/runner/writer_pdf.go index 857007d1..64335705 100644 --- a/pkg/runner/writer_pdf.go +++ b/pkg/runner/writer_pdf.go @@ -19,6 +19,7 @@ package runner import ( _ "embed" "fmt" + "github.com/linuxsuren/api-testing/pkg/logging" "io" @@ -125,3 +126,7 @@ func (w *pdfResultWriter) WithAPICoverage(apiConverage apispec.APICoverage) Repo func (w *pdfResultWriter) WithResourceUsage([]ResourceUsage) ReportResultWriter { return w } + +func (w *pdfResultWriter) GetWriter() io.Writer { + return w.writer +} diff --git a/pkg/runner/writer_std.go b/pkg/runner/writer_std.go index 999b1f82..1ce3bda8 100644 --- a/pkg/runner/writer_std.go +++ b/pkg/runner/writer_std.go @@ -70,6 +70,10 @@ func (w *stdResultWriter) WithResourceUsage([]ResourceUsage) ReportResultWriter return w } +func (w *stdResultWriter) GetWriter() io.Writer { + return w.writer +} + func apiConveragePrint(result []ReportResult, apiConverage apispec.APICoverage, w io.Writer) { covered, total := apiConverageCount(result, apiConverage) if total > 0 { diff --git a/pkg/runner/writer_std_test.go b/pkg/runner/writer_std_test.go index 5c4e19df..20806d91 100644 --- a/pkg/runner/writer_std_test.go +++ b/pkg/runner/writer_std_test.go @@ -110,7 +110,7 @@ Test case count: 1 err := writer.Output(tt.results) assert.Nil(t, err) assert.Equal(t, tt.expect, tt.buf.String()) - + assert.NotNil(t, writer.GetWriter()) assert.NotNil(t, writer.WithResourceUsage(nil)) }) }