diff --git a/.dockerignore b/.dockerignore index c00861a..22818be 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,5 @@ -DockerFile +bundle.dockerfile +standalone.dockerfile .gitattributes .gitignore Readme.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1734be4..3260695 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,8 +6,8 @@ on: tags: - '*' -jobs: - build: +jobs: + build-binaries: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -33,3 +33,35 @@ jobs: artifacts: "*.gz" token: ${{ secrets.GITHUB_TOKEN }} draft: true + + build-docker-standalone: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + name: Check out code + + - uses: mr-smithers-excellent/docker-build-push@v5 + name: Build & push Docker standalone image + with: + image: quest + tags: standalone-latest, standalone-${{ github.ref_name }} + registry: ghcr.io + dockerfile: standalone.dockerfile + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + build-docker-bundle: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + name: Check out code + + - uses: mr-smithers-excellent/docker-build-push@v5 + name: Build & push Docker bundle image + with: + image: quest + tags: bundle-latest, bundle-${{ github.ref_name }} + registry: ghcr.io + dockerfile: bundle.dockerfile + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} \ No newline at end of file diff --git a/README.md b/README.md index ed53dbd..09828fb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -# qest - ## What is qest? A simple, cross platform, command line tool to test MSSQL procedures without the needs of a SSDT project or custom procedures / assemblies. @@ -13,35 +11,55 @@ The tool does not implement a transaction logic for the tests: you have to provi This is by design, to not interfere with the transaction logic that may be implemented in the stored procedures. ## Quickstart -### Local +### Local binary Run tests in a single file: ``` -./qest --file relative/path/to/file.yml --tcs targetDatabaseConnectionString +./qest run --file relative/path/to/file.yml --tcs targetDatabaseConnectionString ``` Run all tests in a folder: ``` -./qest --folder relative/path/to/folder --tcs targetDatabaseConnectionString +./qest run --folder relative/path/to/folder --tcs targetDatabaseConnectionString +``` +Generate templates from the database into an output directory: +``` +./qest run --folder relative/path/to/folder --tcs targetDatabaseConnectionString +``` +### Local container, provided database server +Same options as the local binary version, but with a provided runtime. ``` -### Container +docker run --rm -t \ + -v {full/local/path/to/test/folder}:/tests \ + -v {full/local/path/to/scripts/folder}:/scripts \ + qest:standalone \ + run --folder tests --tcs targetDatabaseConnectionString +``` +or: +``` +docker run --rm -t \ + -v {full/local/path/to/template/folder}:/templates \ + qest:standalone \ + generate --folder templates --tcs targetDatabaseConnectionString +``` + +### Bundle container +This container contains ( ;-) ) Microsoft SQL Server 2019 *and* qest executables, so you can deploy the database and run tests in a pristine environment. You need to provide: -- the `dacpac` file of your database: the folder containing it wil be mounted on the `/quest/db` container folder -- the YAML files: the folder containing them wil be mounted on the `/quest/tests` container folder -- the scripts: the folder containing them wil be mounted on the `/quest/scripts` container folder +- the `dacpac` file of your database: the folder containing it wil be mounted on the `/db` container folder +- the YAML files: the folder containing them wil be mounted on the `/tests` container folder +- the scripts: the folder containing them wil be mounted on the `/scripts` container folder -Please note: for this default image to work, YAML files have to reference the _File_ scripts in the `scripts/{filename}` form. See [docs](docs/YamlFormat.md#script). +Please note: for this default image to work, YAML files have to reference the _File_ scripts in the `scripts/{filename}` form. See [docs](https://github.com/Geims83/qest/wiki/YamlFormat). Run the image binding the `tests`, `scripts` and `db` directories and providing the correct environment variables: ``` -docker run --rm \ - -v {full/local/path/to/test/folder}:/qest/tests \ - -v {full/local/path/to/scripts/folder}:/qest/scripts \ - -v {full/local/path/to/dacpac/folder}:/qest/db \ +docker run --rm -t \ + -v {full/local/path/to/test/folder}:/tests \ + -v {full/local/path/to/scripts/folder}:/scripts \ + -v {full/local/path/to/dacpac/folder}:/db \ --env DACPAC={filenameWithoutExtension} \ - ghcr.io/geims83/qest:latest + qest:bundle ``` -## Samples -See [samples folder](samples/README.md). -## YAML test definition -See [docs](docs/YamlFormat.md). \ No newline at end of file +## Got _qest_ ...ions? +Go to the [wiki](https://github.com/Geims83/qest/wiki)! \ No newline at end of file diff --git a/Dockerfile b/bundle.dockerfile similarity index 54% rename from Dockerfile rename to bundle.dockerfile index 6a02e5f..cd07f7c 100644 --- a/Dockerfile +++ b/bundle.dockerfile @@ -1,7 +1,7 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0 as build COPY ./src ./src -RUN dotnet publish /src/qest/ -o /qest --runtime linux-x64 -c Release --self-contained true /p:PublishSingleFile=true /p:PublishTrimmed=true +RUN dotnet publish /src/qest/ -o /output --runtime linux-x64 -c Release --self-contained true /p:PublishSingleFile=true /p:PublishTrimmed=true FROM mcr.microsoft.com/mssql/server:2019-latest AS mssql USER root @@ -9,12 +9,9 @@ USER root # env vars needed by the mssql image ENV ACCEPT_EULA Y ENV SA_PASSWORD qestDbSecurePassword27! -# -#ENV DACPAC - -WORKDIR /qest -COPY --from=build /qest/qest . +WORKDIR /app +COPY --from=build /output/ . RUN chmod a+x qest @@ -22,12 +19,16 @@ RUN chmod a+x qest RUN apt-get update \ && apt-get install unzip -y -RUN wget -O sqlpackage.zip https://go.microsoft.com/fwlink/?linkid=2143497 +RUN wget -O sqlpackage.zip https://aka.ms/sqlpackage-linux RUN unzip sqlpackage.zip RUN chmod a+x sqlpackage +WORKDIR / + # Launch SQL Server, confirm startup is complete, deploy the DACPAC, run tests. # See https://stackoverflow.com/a/51589787/488695 + ENTRYPOINT ["sh", "-c", "( /opt/mssql/bin/sqlservr & ) | grep -q \"Service Broker manager has started\" \ - && ./sqlpackage /a:Publish /sf:db/${DACPAC}.dacpac /tsn:. /tdn:$DACPAC /tu:sa /tp:$SA_PASSWORD \ - && ./qest --folder tests --tcs \"Server=localhost,1433;Initial Catalog=${DACPAC};User Id=sa;Password=${SA_PASSWORD}\""] \ No newline at end of file + && PATH='$PATH':/app \ + && sqlpackage /a:Publish /sf:db/${DACPAC}.dacpac /tsn:. /tdn:$DACPAC /tu:sa /tp:$SA_PASSWORD \ + && qest run --folder tests --tcs \"Server=localhost,1433;Initial Catalog=${DACPAC};User Id=sa;Password=${SA_PASSWORD}\""] \ No newline at end of file diff --git a/docs/YamlFormat.md b/docs/YamlFormat.md deleted file mode 100644 index 187eee6..0000000 --- a/docs/YamlFormat.md +++ /dev/null @@ -1,154 +0,0 @@ -# YAML test definition - -## Full Definition -``` -- name: - before: - - type: - values: - - - command: - commandText: - parameters: - - name: - type: - value: - results: - resultSets: - - name: - rowNumber: - columns: - - name: - type: - outputParameters: - - name: - type: - value: - returnCode: - asserts: - - sqlQuery: - scalarType: - scalarValue: - after: - - type: - values: - - -``` -## Sections -### name -_Required_
-The full name of the test. - -### before -_Optional_
-Array of [Scripts](#script) object that runs before the command is executed.
-Before scripts run in a single transaction. - -### command -__Mandatory__
-A single element representing the command (Stored Procedure) to run. -|Attribute|Description|M/O|Notes| -|---|---|:---:|---| -|__commandText__|The Stored Procedure to run|__M__|| -|__parameters__|A list of [Parameters](#parameter)|_O_|| - -### results -_Optional_
-The definition of the results to verify. -|Attribute|Description|M/O|Notes| -|---|---|:---:|---| -|resultSets|A list of [ResultSets](#resultset)|_O_|| -|outputParameters|A list of [Parameters](#parameter)|_O_|| -|returnCode| The return code value |_O_|| - -### asserts -_Optional_
-The definition of the asserts to run.
-A list of [Assert](#assert) objects. - -### after -_Optional_
-Array of [Scripts](#script) object that runs after the command is executed.
-Generally used to delete the data provided/generated by the test. - -## Types - -### Assert -Definition of an assert. -``` -sqlQuery: -scalarType: -scalarValue: -``` -|Attribute|Description|M/O|Notes| -|---|---|:---:|---| -|__sqlQuery__|Query to run|__M__|Must return a single value as a result set (one row, one column)| -|__scalarType__|Expected type of the scalar returned from the assert|__M__|See [Types](#type)| -|__scalarValue__|Expected value the scalar returned from the assert|__M__|| - -### Parameter -Definition of a parameter. -``` -name: -type: -value: -``` -|Attribute|Description|M/O|Notes| -|---|---|:---:|---| -|__name__|Name of the parameter|__M__|Without __@__| -|__type__|Type of the parameter|__M__|See [Types](#type)| -|__value__|Value of the parameter|_O_|Mandatory depends on what is defined in the database| - -### ResultSet -Definition of a result set. -``` -- name: - rowNumber: - columns: - - name: - type: -``` -|Attribute|Description|M/O|Notes| -|---|---|:---:|---| -|__name__|Name of the result set|__M__|| -|__columns__|Definition of the columns of the result set|__M__|| -|__rowNumber__|Expected number of rows|_O_|| - -Each column is defined as: -|Attribute|Description|M/O|Notes| -|---|---|:---:|---| -|__name__|Name of the column of the result set|__M__|| -|__type__|Type of the column|See [Types](#type)|__M__|| - -### Script -Definition of a script to run. -``` -type: Inline | File -values: - - -``` -|Attribute|Description|M/O|Notes| -|---|---|:---:|---| -|__type__|Type of the script|_Inline_ or _File_|__M__| -|__value__|If __type__ is _Inline_: a list of SQL commands
If __type__ is _File_: a list of path to text files |__M__|Files path shoud be relative to the __qest__ excutable| - -Each __Script__ entity is ran atomically: the strings / files are concatenated in a single SQL batch joined by ";". - -### Type -The types used are the text representation of the _SqlDBType_ C# enum. -The supported types are: - - Bit - - TinyInt - - SmallInt - - BigInt - - Int - - Float - - Real - - Decimal - - Money - - NVarChar - - DateTime - - DateTime2 - - DateTimeOffset - - Date - - Time \ No newline at end of file diff --git a/samples/Dockerfile b/samples/Dockerfile index f5db1f2..5ed5116 100644 --- a/samples/Dockerfile +++ b/samples/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /build COPY ./sampleDb . RUN dotnet build . -c Release -o /dacpac -FROM ghcr.io/geims83/qest:latest +FROM qest:bundle ENV DACPAC=sampleDb diff --git a/samples/README.md b/samples/README.md deleted file mode 100644 index de46866..0000000 --- a/samples/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# How to run the provided samples - -## Step 1 - Download this folder -Clone or download this repo. - -## Step 2 -### Build your local image... -Open a terminal in the folder where you have downloaded the data. Then run: -``` -docker build . -t qestsamples -docker run -t qestsamples -``` -### ...or run directly from the registry -Build the sample database: -``` -dotnet build ./sampleDb/ -c Release -o ./dacpac -``` - -Then run the command: -``` -docker run --rm -t \ - -v {full/local/path}/tests:/qest/tests \ - -v {full/local/path}/scripts:/qest/scripts \ - -v {full/local/path}/dacpac:/qest/db \ - --env DACPAC={filename} \ - ghcr.io/geims83/qest:latest -``` - -## Step 3: you're done! -The output should look like this: -``` -Running Test: SampleSP - Ok -Running Before scripts... -Completed. -Checking ResultSet: sampleSpRS1 -Result sampleSpRS1: OK -Checking Output Parameter: oldValue -Result oldValue: 0 == 0 -Checking Return Code -Return Code: 0 == 0 -Assert SELECT COUNT(*) FROM dbo.SampleTable WHERE [Value] = 1: 1 == 1 -Running After scripts... -Completed. -Test SampleSP - Ok: OK -``` - -You have run a couple of tests on the stored procedure, and everything looks fine and green! - -# ...Wait a minute! -Ok, let's assume you want to be sure that everything is actually being tested - let's go brake things. -Go to [the stored procedure definition](sampleDb/dbo/Stored%20Procedures/SampleSP.sql) and change a little bit of logic: let's say that we want to know how many rows are updated during the operation. - -At line 9, start by declaring **@rc** as 0: -``` -DECLARE @rc TINYINT = 0: -``` - -And at line 29: -``` -SET @rc = @@ROWCOUNT -``` - -Now build the image again: you should get an error, and a log like: -``` -Running Test: SampleSP - Ok -Running Before scripts... -Completed. -Checking ResultSet: sampleSpRS1 -Result sampleSpRS1: OK -Checking Output Parameter: oldValue -Result oldValue: 0 == 0 -Checking Return Code -Return Code: 1 != 0 -Assert SELECT COUNT(*) FROM dbo.SampleTable WHERE [IntValue] = 1: 1 == 1 -Running After scripts... -Completed. -Test SampleSP - Ok: KO -``` -As you see, the first test checked the resultset - good, the output parameter - good , but the return code expected was **0** and we got a **1**. - -Now: let's go to the [test definition](tests/sampleSp.yml).
-As we expect to update one row when we execute the procedures with these parameters, we have to change row 28 from **0** to **1**.
-But wait! We have another test in the definition!
-We have to change row 52 too, from **1** to **0** (in the negative test, we don't load the row we are trying to update, so @@ROWCOUNT is expected to be 0). - -Now build / run the image again and everything runs smoothly as previously. \ No newline at end of file diff --git a/samples/tests/sampleSp.yml b/samples/tests/sampleSp.yml index a1c5847..20d0de7 100644 --- a/samples/tests/sampleSp.yml +++ b/samples/tests/sampleSp.yml @@ -1,112 +1,105 @@ -- name: SampleSP - Ok +- name: SampleSP + variables: + nameVar: SampleName + newValueVar: 1 before: - - type: File - values: - - scripts/SampleData.sql - command: - commandText: dbo.SampleSp - parameters: - - name: name - type: NVarChar - value: SampleName - - name: newValue - type: Int - value: 1 - results: - resultSets: - - name: sampleSpRS1 - rowNumber: 1 - columns: - - name: Name - type: NVarChar - - name: BitValue - type: Bit - - name: TinyIntValue - type: TinyInt - - name: SmallintValue - type: Smallint - - name: IntValue - type: Int - - name: BigIntValue - type: BigInt - - name: FloatValue - type: Float - - name: RealtValue - type: Real - - name: DecimalValue - type: Decimal - - name: MoneyValue - type: Money - - name: DateTimeValue - type: DateTime - - name: DateTime2Value - type: DateTime2 - - name: DateTimeOffsetValue - type: DateTimeOffset - - name: DateValue - type: Date - - name: TimeValue - type: Time - outputParameters: - - name: oldValue - type: Int - value: 0 - returnCode: 0 - asserts: - - sqlQuery: SELECT COUNT(*) FROM dbo.SampleTable WHERE [IntValue] = 1 - scalarType: Int - scalarValue: 1 + - type: File + values: + - scripts/SampleData.sql + steps: + - name: Test OK + command: + commandText: dbo.SampleSp + parameters: + name: "{nameVar}" + newValue: "{newValueVar}" + results: + resultSets: + - name: sampleSpRS1 + rowNumber: 1 + columns: + - name: Name + type: NVarChar + - name: BitValue + type: Bit + - name: TinyIntValue + type: TinyInt + - name: SmallintValue + type: Smallint + - name: IntValue + type: Int + - name: BigIntValue + type: BigInt + - name: FloatValue + type: Float + - name: RealtValue + type: Real + - name: DecimalValue + type: Decimal + - name: MoneyValue + type: Money + - name: DateTimeValue + type: DateTime + - name: DateTime2Value + type: DateTime2 + - name: DateTimeOffsetValue + type: DateTimeOffset + - name: DateValue + type: Date + - name: TimeValue + type: Time + outputParameters: + - name: oldValue + type: Int + value: 0 + returnCode: 0 + asserts: + - sqlQuery: SELECT COUNT(*) FROM dbo.SampleTable WHERE [IntValue] = {newValueVar} + scalarType: Int + scalarValue: 1 + - name: Test KO + command: + commandText: dbo.SampleSp + parameters: + name: "NoMatchData" + newValue: "{newValueVar}" + results: + resultSets: + - name: sampleSpRS1 + rowNumber: 0 + columns: + - name: Name + type: NVarChar + - name: BitValue + type: Bit + - name: TinyIntValue + type: TinyInt + - name: SmallintValue + type: Smallint + - name: IntValue + type: Int + - name: BigIntValue + type: BigInt + - name: FloatValue + type: Float + - name: RealtValue + type: Real + - name: DecimalValue + type: Decimal + - name: MoneyValue + type: Money + - name: DateTimeValue + type: DateTime + - name: DateTime2Value + type: DateTime2 + - name: DateTimeOffsetValue + type: DateTimeOffset + - name: DateValue + type: Date + - name: TimeValue + type: Time + returnCode: 1 after: - - type: File - values: - - scripts/DeleteTestData.sql -- name: SampleSP - KO - command: - commandText: dbo.SampleSp - parameters: - - name: name - type: NVarChar - value: NoMatchData - - name: newValue - type: Int - value: 1 - results: - resultSets: - - name: sampleSpRS1 - rowNumber: 0 - columns: - - name: Name - type: NVarChar - - name: BitValue - type: Bit - - name: TinyIntValue - type: TinyInt - - name: SmallintValue - type: Smallint - - name: IntValue - type: Int - - name: BigIntValue - type: BigInt - - name: FloatValue - type: Float - - name: RealtValue - type: Real - - name: DecimalValue - type: Decimal - - name: MoneyValue - type: Money - - name: DateTimeValue - type: DateTime - - name: DateTime2Value - type: DateTime2 - - name: DateTimeOffsetValue - type: DateTimeOffset - - name: DateValue - type: Date - - name: TimeValue - type: Time - returnCode: 1 - after: - - type: File - values: - - scripts/DeleteTestData.sql \ No newline at end of file + - type: File + values: + - scripts/DeleteTestData.sql diff --git a/src/TestBase/Script.cs b/src/TestBase/Script.cs index ea0d873..4b9182c 100644 --- a/src/TestBase/Script.cs +++ b/src/TestBase/Script.cs @@ -5,7 +5,7 @@ public class Script public ScriptType Type { get; set; } public List Values { get; set; } - public string Compact() + public string Compact(Dictionary? variables) { if (Values == null) throw new ArgumentNullException(nameof(Values)); @@ -13,7 +13,7 @@ public string Compact() switch (this.Type) { case ScriptType.Inline: - return string.Join(";", Values); + return string.Join(";", Values).ReplaceVars(variables); case ScriptType.File: List list = new List(); @@ -28,7 +28,7 @@ public string Compact() else throw new FileNotFoundException(null, item); } - return string.Join(";", list); + return string.Join(";", list).ReplaceVars(variables); default: throw new ArgumentException(nameof(Type)); } diff --git a/src/TestBase/Scripts.cs b/src/TestBase/Scripts.cs new file mode 100644 index 0000000..d1f7ff2 --- /dev/null +++ b/src/TestBase/Scripts.cs @@ -0,0 +1,33 @@ +using System.Data.SqlClient; + +namespace TestBase +{ + public class Scripts : List