├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── DockerComposeFixture.Tests ├── Compose │ ├── ObserverToQueueTests.cs │ └── ProcessRunnerTests.cs ├── DockerComposeFixture.Tests.csproj ├── DockerFixtureTests.cs ├── IntegrationTests.cs ├── Logging │ └── LoggerTests.cs ├── Utils │ └── ObservableCounter.cs └── xunit.runner.json ├── DockerComposeFixture.sln ├── DockerComposeFixture ├── Compose │ ├── DockerCompose.cs │ ├── IDockerCompose.cs │ ├── ObserverToQueue.cs │ └── ProcessRunner.cs ├── DockerComposeFixture.csproj ├── DockerComposeFixture.nuspec ├── DockerFixture.cs ├── DockerFixtureOptions.cs ├── Exceptions │ └── DockerComposeException.cs ├── IDockerFixtureOptions.cs └── Logging │ ├── ConsoleLogger.cs │ ├── FileLogger.cs │ ├── ILogger.cs │ ├── ListLogger.cs │ ├── LoggingExtensionMethods.cs │ └── XUnitLogger.cs ├── LICENSE └── README.md /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: 8.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | - name: Package 27 | if: ${{ github.event_name == 'push' }} 28 | run: dotnet pack -c Release -o . ./DockerComposeFixture/DockerComposeFixture.csproj 29 | - name: Publish 30 | if: ${{ github.event_name == 'push' }} 31 | run: dotnet nuget push *.nupkg -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | 56 | # StyleCop 57 | StyleCopReport.xml 58 | 59 | # Files built by Visual Studio 60 | *_i.c 61 | *_p.c 62 | *_i.h 63 | *.ilk 64 | *.meta 65 | *.obj 66 | *.iobj 67 | *.pch 68 | *.pdb 69 | *.ipdb 70 | *.pgc 71 | *.pgd 72 | *.rsp 73 | *.sbr 74 | *.tlb 75 | *.tli 76 | *.tlh 77 | *.tmp 78 | *.tmp_proj 79 | *.log 80 | *.vspscc 81 | *.vssscc 82 | .builds 83 | *.pidb 84 | *.svclog 85 | *.scc 86 | 87 | # Chutzpah Test files 88 | _Chutzpah* 89 | 90 | # Visual C++ cache files 91 | ipch/ 92 | *.aps 93 | *.ncb 94 | *.opendb 95 | *.opensdf 96 | *.sdf 97 | *.cachefile 98 | *.VC.db 99 | *.VC.VC.opendb 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | *.vspx 105 | *.sap 106 | 107 | # Visual Studio Trace Files 108 | *.e2e 109 | 110 | # TFS 2012 Local Workspace 111 | $tf/ 112 | 113 | # Guidance Automation Toolkit 114 | *.gpState 115 | 116 | # ReSharper is a .NET coding add-in 117 | _ReSharper*/ 118 | *.[Rr]e[Ss]harper 119 | *.DotSettings.user 120 | 121 | # JustCode is a .NET coding add-in 122 | .JustCode 123 | 124 | # TeamCity is a build add-in 125 | _TeamCity* 126 | 127 | # DotCover is a Code Coverage Tool 128 | *.dotCover 129 | 130 | # AxoCover is a Code Coverage Tool 131 | .axoCover/* 132 | !.axoCover/settings.json 133 | 134 | # Visual Studio code coverage results 135 | *.coverage 136 | *.coveragexml 137 | 138 | # NCrunch 139 | _NCrunch_* 140 | .*crunch*.local.xml 141 | nCrunchTemp_* 142 | 143 | # MightyMoose 144 | *.mm.* 145 | AutoTest.Net/ 146 | 147 | # Web workbench (sass) 148 | .sass-cache/ 149 | 150 | # Installshield output folder 151 | [Ee]xpress/ 152 | 153 | # DocProject is a documentation generator add-in 154 | DocProject/buildhelp/ 155 | DocProject/Help/*.HxT 156 | DocProject/Help/*.HxC 157 | DocProject/Help/*.hhc 158 | DocProject/Help/*.hhk 159 | DocProject/Help/*.hhp 160 | DocProject/Help/Html2 161 | DocProject/Help/html 162 | 163 | # Click-Once directory 164 | publish/ 165 | 166 | # Publish Web Output 167 | *.[Pp]ublish.xml 168 | *.azurePubxml 169 | # Note: Comment the next line if you want to checkin your web deploy settings, 170 | # but database connection strings (with potential passwords) will be unencrypted 171 | *.pubxml 172 | *.publishproj 173 | 174 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 175 | # checkin your Azure Web App publish settings, but sensitive information contained 176 | # in these scripts will be unencrypted 177 | PublishScripts/ 178 | 179 | # NuGet Packages 180 | *.nupkg 181 | # The packages folder can be ignored because of Package Restore 182 | **/[Pp]ackages/* 183 | # except build/, which is used as an MSBuild target. 184 | !**/[Pp]ackages/build/ 185 | # Uncomment if necessary however generally it will be regenerated when needed 186 | #!**/[Pp]ackages/repositories.config 187 | # NuGet v3's project.json files produces more ignorable files 188 | *.nuget.props 189 | *.nuget.targets 190 | 191 | # Microsoft Azure Build Output 192 | csx/ 193 | *.build.csdef 194 | 195 | # Microsoft Azure Emulator 196 | ecf/ 197 | rcf/ 198 | 199 | # Windows Store app package directories and files 200 | AppPackages/ 201 | BundleArtifacts/ 202 | Package.StoreAssociation.xml 203 | _pkginfo.txt 204 | *.appx 205 | 206 | # Visual Studio cache files 207 | # files ending in .cache can be ignored 208 | *.[Cc]ache 209 | # but keep track of directories ending in .cache 210 | !*.[Cc]ache/ 211 | 212 | # Others 213 | ClientBin/ 214 | ~$* 215 | *~ 216 | *.dbmdl 217 | *.dbproj.schemaview 218 | *.jfm 219 | *.pfx 220 | *.publishsettings 221 | orleans.codegen.cs 222 | 223 | # Including strong name files can present a security risk 224 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 225 | #*.snk 226 | 227 | # Since there are multiple workflows, uncomment next line to ignore bower_components 228 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 229 | #bower_components/ 230 | 231 | # RIA/Silverlight projects 232 | Generated_Code/ 233 | 234 | # Backup & report files from converting an old project file 235 | # to a newer Visual Studio version. Backup files are not needed, 236 | # because we have git ;-) 237 | _UpgradeReport_Files/ 238 | Backup*/ 239 | UpgradeLog*.XML 240 | UpgradeLog*.htm 241 | ServiceFabricBackup/ 242 | *.rptproj.bak 243 | 244 | # SQL Server files 245 | *.mdf 246 | *.ldf 247 | *.ndf 248 | 249 | # Business Intelligence projects 250 | *.rdl.data 251 | *.bim.layout 252 | *.bim_*.settings 253 | *.rptproj.rsuser 254 | 255 | # Microsoft Fakes 256 | FakesAssemblies/ 257 | 258 | # GhostDoc plugin setting file 259 | *.GhostDoc.xml 260 | 261 | # Node.js Tools for Visual Studio 262 | .ntvs_analysis.dat 263 | node_modules/ 264 | 265 | # Visual Studio 6 build log 266 | *.plg 267 | 268 | # Visual Studio 6 workspace options file 269 | *.opt 270 | 271 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 272 | *.vbw 273 | 274 | # Visual Studio LightSwitch build output 275 | **/*.HTMLClient/GeneratedArtifacts 276 | **/*.DesktopClient/GeneratedArtifacts 277 | **/*.DesktopClient/ModelManifest.xml 278 | **/*.Server/GeneratedArtifacts 279 | **/*.Server/ModelManifest.xml 280 | _Pvt_Extensions 281 | 282 | # Paket dependency manager 283 | .paket/paket.exe 284 | paket-files/ 285 | 286 | # FAKE - F# Make 287 | .fake/ 288 | 289 | # JetBrains Rider 290 | .idea/ 291 | *.sln.iml 292 | 293 | # CodeRush 294 | .cr/ 295 | 296 | # Python Tools for Visual Studio (PTVS) 297 | __pycache__/ 298 | *.pyc 299 | 300 | # Cake - Uncomment if you are using it 301 | # tools/** 302 | # !tools/packages.config 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | 325 | # NVidia Nsight GPU debugger configuration file 326 | *.nvuser 327 | 328 | # MFractors (Xamarin productivity tool) working folder 329 | .mfractor/ -------------------------------------------------------------------------------- /DockerComposeFixture.Tests/Compose/ObserverToQueueTests.cs: -------------------------------------------------------------------------------- 1 | using DockerComposeFixture.Compose; 2 | using DockerComposeFixture.Tests.Utils; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace DockerComposeFixture.Tests.Compose 7 | { 8 | public class ObserverToQueueTests 9 | { 10 | [Fact] 11 | public async Task OnNext_EnqueuesItems_WhenCalled() 12 | { 13 | var observerToQueue = new ObserverToQueue(); 14 | var counter = new ObservableCounter(); 15 | counter.Subscribe(observerToQueue); 16 | var task = new Task(() => counter.Count()); 17 | Assert.Empty(observerToQueue.Queue); 18 | 19 | task.Start(); 20 | await task; 21 | 22 | Assert.Equal(10, observerToQueue.Queue.Count); 23 | Assert.Equal("1,2,3,4,5,6,7,8,9,10".Split(","), 24 | observerToQueue.Queue.ToArray()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DockerComposeFixture.Tests/Compose/ProcessRunnerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using DockerComposeFixture.Compose; 4 | using DockerComposeFixture.Logging; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace DockerComposeFixture.Tests.Compose 9 | { 10 | public class ProcessRunnerTests 11 | { 12 | [Fact] 13 | public void Execute_ReturnsOutput_WhenCalled() 14 | { 15 | var logger = new Mock(); 16 | var psi = new ProcessStartInfo("echo", "\"test1\ntest2\ntest3\""); 17 | if (Environment.OSVersion.Platform == PlatformID.Win32NT) 18 | { 19 | psi = new ProcessStartInfo("cmd.exe", "/C \"echo test1& echo test2& echo test3\""); 20 | } 21 | var runner = new ProcessRunner(psi); 22 | runner.Subscribe(logger.Object); 23 | runner.Execute(); 24 | logger.Verify(l => l.OnNext("test1"), Times.Once); 25 | logger.Verify(l => l.OnNext("test2"), Times.Once); 26 | logger.Verify(l => l.OnNext("test3"), Times.Once); 27 | logger.Verify(l => l.OnNext(It.IsAny()), Times.Exactly(3)); 28 | logger.Verify(l => l.OnCompleted(), Times.Once); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DockerComposeFixture.Tests/DockerComposeFixture.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | net8.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | PreserveNewest 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /DockerComposeFixture.Tests/DockerFixtureTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using DockerComposeFixture.Logging; 8 | using DockerComposeFixture.Compose; 9 | using DockerComposeFixture.Exceptions; 10 | using Moq; 11 | using Xunit; 12 | 13 | namespace DockerComposeFixture.Tests 14 | { 15 | public class DockerFixtureTests 16 | { 17 | private const int NumberOfMsInOneSec = 10; // makes testing faster 18 | private const int ComposeUpRunDurationMs = 5000; 19 | 20 | [Fact] 21 | public void Init_StopsDocker_IfAlreadyRunning() 22 | { 23 | var compose = new Mock(); 24 | compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec); 25 | compose.Setup(c => c.PsWithJsonFormat()) 26 | .Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" }); 27 | compose.Setup(c => c.Up()).Returns(Task.Delay(ComposeUpRunDurationMs)); 28 | 29 | new DockerFixture(null) 30 | .Init(new[] { Path.GetTempFileName() }, "up", "down", 120, null, compose.Object); 31 | compose.Verify(c => c.Init(It.IsAny(), "up", "down"), Times.Once); 32 | compose.Verify(c => c.Down(), Times.Once); 33 | } 34 | 35 | [Fact] 36 | public void Init_InitialisesDocker_WhenCalled() 37 | { 38 | var compose = new Mock(); 39 | compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec); 40 | compose.SetupSequence(c => c.PsWithJsonFormat()) 41 | .Returns(new[] { "non-json-message" }) 42 | .Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" }); 43 | compose.Setup(c => c.Up()).Returns(Task.Delay(ComposeUpRunDurationMs)); 44 | 45 | var tmp = Path.GetTempFileName(); 46 | new DockerFixture(null) 47 | .Init(new[] { tmp }, "up", "down", 120, null, compose.Object); 48 | compose.Verify(c => c.Init($"-f \"{tmp}\"", "up", "down"), Times.Once); 49 | compose.Verify(c => c.Up(), Times.Once); 50 | } 51 | 52 | [Fact] 53 | public void InitOnce_InitialisesDockerOnce_WhenCalledTwice() 54 | { 55 | var compose = new Mock(); 56 | compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec); 57 | compose.Setup(c => c.Up()).Returns(Task.Delay(ComposeUpRunDurationMs)); 58 | compose.SetupSequence(c => c.PsWithJsonFormat()) 59 | .Returns(new[] { "non-json-message" }) 60 | .Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" }); 61 | 62 | var tmp = Path.GetTempFileName(); 63 | var fixture = new DockerFixture(null); 64 | 65 | fixture.InitOnce(() => new DockerFixtureOptions { DockerComposeFiles = new[] { tmp } }, compose.Object); 66 | fixture.InitOnce(() => new DockerFixtureOptions { DockerComposeFiles = new[] { tmp } }, compose.Object); 67 | compose.Verify(c => c.Init(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); 68 | compose.Verify(c => c.Up(), Times.Once); 69 | } 70 | 71 | [Fact] 72 | public void Init_Waits_UntilUpTestIsTrue() 73 | { 74 | var compose = new Mock(); 75 | compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec); 76 | compose.Setup(c => c.Up()).Returns(Task.Delay(5000)); 77 | compose.Setup(c => c.PsWithJsonFormat()) 78 | .Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" }); 79 | const string successText = "Everything is up"; 80 | 81 | var logger = new ListLogger(); 82 | var task = new Task(() => 83 | new DockerFixture(null) 84 | .Init(new[] { Path.GetTempFileName() }, "up", "down", 120, 85 | outputLinesFromUp => outputLinesFromUp.Contains(successText), 86 | compose.Object, new []{ logger})); 87 | task.Start(); 88 | Thread.Sleep(100); 89 | logger.OnNext("foo"); 90 | logger.OnNext(successText); 91 | task.Wait(); 92 | 93 | compose.Verify(c => c.Init(It.IsAny(), "up", "down"), Times.Once); 94 | compose.Verify(c => c.Up(), Times.Once); 95 | } 96 | 97 | [Fact] 98 | public void Init_Throws_IfTestIsNeverTrue() 99 | { 100 | var compose = new Mock(); 101 | compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec); 102 | compose.Setup(c => c.PsWithJsonFormat()) 103 | .Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" }); 104 | const string successText = "Everything is up"; 105 | var logger = new ListLogger(); 106 | compose.SetupGet(c => c.Logger).Returns(new []{ logger}); 107 | 108 | Assert.Throws(() => 109 | { 110 | var task = new Task(() => 111 | new DockerFixture(null) 112 | .Init(new[] { Path.GetTempFileName() }, "up", "down", 120, 113 | outputLinesFromUp => outputLinesFromUp.Contains(successText), 114 | compose.Object)); 115 | task.Start(); 116 | logger.OnNext("foo"); 117 | logger.OnNext("bar"); 118 | Thread.Sleep(100); 119 | logger.OnNext("foo"); 120 | logger.OnNext("bar"); 121 | task.Wait(); 122 | }); 123 | } 124 | 125 | [Fact] 126 | public void Init_MonitorsServices_WhenTheyStartSlowly() 127 | { 128 | Stopwatch stopwatch = new Stopwatch(); 129 | var compose = new Mock(); 130 | compose.Setup(c => c.PauseMs).Returns(100); 131 | compose.Setup(c => c.Up()) 132 | .Returns(Task.Delay(ComposeUpRunDurationMs)) 133 | .Callback(() => stopwatch.Start()); 134 | compose.Setup(c => c.PsWithJsonFormat()).Returns(() => 135 | { 136 | if (!stopwatch.IsRunning) 137 | { 138 | return new[] { "non-json-message" }; 139 | } 140 | var firstServiceStatus = stopwatch.ElapsedMilliseconds < 1000 ? "Starting" : "Up 2 seconds"; 141 | var secondServiceStatus = stopwatch.ElapsedMilliseconds < 3000 ? "Starting" : "Up 4 seconds"; 142 | return new[] 143 | { 144 | "blah", 145 | $"{{ \"Status\": \"{firstServiceStatus}\" }}", 146 | $"{{ \"Status\": \"{secondServiceStatus}\" }}" 147 | }; 148 | }); 149 | 150 | new DockerFixture(null).Init(new[] { Path.GetTempFileName() }, "up", "down", 120, null, compose.Object); 151 | 152 | compose.Verify(c => c.Up(), Times.Once); 153 | compose.Verify(c => c.PsWithJsonFormat(), Times.AtLeast(5)); 154 | } 155 | 156 | [Fact] 157 | public void Init_Throws_WhenServicesFailToStart() 158 | { 159 | var compose = new Mock(); 160 | compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec); 161 | bool firstTime = true; 162 | compose.Setup(c => c.PsWithJsonFormat()) 163 | .Returns(() => 164 | { 165 | var data = firstTime ? new[] { "non-json-message" } : new[] { "--------", " Down ", " Down " }; 166 | firstTime = false; 167 | return data; 168 | }); 169 | 170 | Assert.Throws(() => 171 | new DockerFixture(null).Init(new[] { Path.GetTempFileName() }, "up", "down", 120, null, compose.Object)); 172 | } 173 | 174 | [Fact] 175 | public void Init_Throws_DockerComposeExitsPrematurely() 176 | { 177 | var compose = new Mock(); 178 | compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec); 179 | compose.Setup(c => c.Up()).Returns(Task.CompletedTask); 180 | 181 | Assert.Throws(() => 182 | new DockerFixture(null).Init(new[] { Path.GetTempFileName() }, "up", "down", 120, null, compose.Object)); 183 | compose.Verify(c => c.PsWithJsonFormat(), Times.Once); 184 | } 185 | 186 | [Fact] 187 | public void Init_Throws_WhenYmlFileDoesntExist() 188 | { 189 | var compose = new Mock(); 190 | compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec); 191 | compose.Setup(c => c.PsWithJsonFormat()) 192 | .Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" }); 193 | 194 | string fileDoesntExist = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); 195 | Assert.Throws(() => 196 | new DockerFixture(null).Init(new[] { fileDoesntExist }, "up", "down", 120, null, compose.Object)); 197 | } 198 | 199 | [Fact] 200 | public void Init_Throws_WhenDockerComposeFilesAreMissing() 201 | { 202 | var compose = new Mock(); 203 | 204 | Assert.Throws(() => 205 | new DockerFixture(null).Init( 206 | () => new DockerFixtureOptions { DockerComposeFiles = new string[0] }, 207 | compose.Object)); 208 | Assert.Throws(() => 209 | new DockerFixture(null).Init( 210 | () => new DockerFixtureOptions { DockerComposeFiles = null }, 211 | compose.Object)); 212 | } 213 | 214 | [Fact] 215 | public void Init_Throws_WhenStartupTimeoutSecsIsLessThanOne() 216 | { 217 | var compose = new Mock(); 218 | 219 | Assert.Throws(() => 220 | new DockerFixture(null).Init( 221 | () => new DockerFixtureOptions { DockerComposeFiles = new[] { "docker-compose.yml" }, StartupTimeoutSecs = 0 }, 222 | compose.Object)); 223 | Assert.Throws(() => 224 | new DockerFixture(null).Init( 225 | () => new DockerFixtureOptions { DockerComposeFiles = new[] { "docker-compose.yml" }, StartupTimeoutSecs = -1 }, 226 | compose.Object)); 227 | } 228 | 229 | [Fact] 230 | public void Dispose_CallsDown_WhenRun() 231 | { 232 | var compose = new Mock(); 233 | compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec); 234 | compose.Setup(c => c.Up()).Returns(Task.Delay(ComposeUpRunDurationMs)); 235 | compose.SetupSequence(c => c.PsWithJsonFormat()) 236 | .Returns(new[] { "non-json-output" }) 237 | .Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" }); 238 | 239 | var fixture = new DockerFixture(null); 240 | fixture.Init(new[] { Path.GetTempFileName() }, "up", "down", 120, null, compose.Object); 241 | fixture.Dispose(); 242 | 243 | compose.Verify(c => c.Down(), Times.Once); 244 | } 245 | 246 | [Fact] 247 | public void Dispose_WithoutInit_DoesNotThrow() 248 | { 249 | var fixture = new DockerFixture(null); 250 | fixture.Dispose(); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /DockerComposeFixture.Tests/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace DockerComposeFixture.Tests 9 | { 10 | public class IntegrationTests : IClassFixture, IDisposable 11 | { 12 | private readonly string dockerComposeFile; 13 | 14 | private const string DockerCompose = @" 15 | version: '3.4' 16 | services: 17 | echo_server: 18 | image: hashicorp/http-echo 19 | ports: 20 | - 12871:8080 21 | command: -listen=:8080 -text=""hello world"" 22 | "; 23 | 24 | public IntegrationTests(DockerFixture dockerFixture) 25 | { 26 | this.dockerComposeFile = Path.GetTempFileName(); 27 | File.WriteAllText(this.dockerComposeFile, DockerCompose); 28 | 29 | DockerFixture.Kill("echo_server").Wait(); 30 | dockerFixture.InitOnce(() => new DockerFixtureOptions 31 | { 32 | DockerComposeFiles = new[] { this.dockerComposeFile }, 33 | CustomUpTest = output => output.Any(l => l.Contains("server is listening")) 34 | }); 35 | } 36 | 37 | [Fact] 38 | public async Task EchoServer_SaysHello_WhenCalled() 39 | { 40 | var client = new HttpClient(); 41 | var response = await client.GetStringAsync("http://localhost:12871"); 42 | Assert.Contains("hello world", response); 43 | } 44 | 45 | public void Dispose() 46 | { 47 | File.Delete(this.dockerComposeFile); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /DockerComposeFixture.Tests/Logging/LoggerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using DockerComposeFixture.Logging; 6 | using DockerComposeFixture.Tests.Utils; 7 | using FluentAssertions; 8 | using Xunit; 9 | 10 | namespace DockerComposeFixture.Tests.Logging 11 | { 12 | public class LoggerTests 13 | { 14 | [Fact] 15 | public async Task OnNext_LogsItemsToFile_WhenCalled() 16 | { 17 | var tmpFile = Path.GetTempFileName(); 18 | 19 | int GetFileLineCount(string file) 20 | { 21 | using (var fs = new FileStream(file, FileMode.OpenOrCreate, FileAccess.Read, FileShare.Write)) 22 | using (var reader = new StreamReader(fs)) 23 | { 24 | return reader.ReadToEnd() 25 | .Split(Environment.NewLine) 26 | .Count(l => l.Length > 0); 27 | } 28 | } 29 | 30 | var loggers = new ILogger[]{ new ListLogger(), new FileLogger(tmpFile), new ConsoleLogger() }; 31 | var counter = new ObservableCounter(); 32 | foreach (var logger in loggers) 33 | { 34 | counter.Subscribe(logger); 35 | } 36 | 37 | var task = new Task(() => counter.Count(delay: 10)); 38 | task.Start(); 39 | await task; 40 | 41 | var fileLineCount = GetFileLineCount(tmpFile); 42 | fileLineCount.Should().Be(10); 43 | var lines = File.ReadAllLines(tmpFile); 44 | lines.Should().BeEquivalentTo("1,2,3,4,5,6,7,8,9,10".Split(",")); 45 | File.Delete(tmpFile); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DockerComposeFixture.Tests/Utils/ObservableCounter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | 5 | namespace DockerComposeFixture.Tests.Utils 6 | { 7 | public class ObservableCounter:IObservable 8 | { 9 | private readonly List> observalbes = new List>(); 10 | 11 | public IDisposable Subscribe(IObserver observer) 12 | { 13 | this.observalbes.Add(observer); 14 | return null; 15 | } 16 | 17 | public void Count(int min = 1, int max = 10, int delay = 10) 18 | { 19 | for (int i = min; i <= max; i++) 20 | { 21 | this.observalbes.ForEach(o => o.OnNext(i.ToString())); 22 | Thread.Sleep(delay); 23 | } 24 | this.observalbes.ForEach(o => o.OnCompleted()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DockerComposeFixture.Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { "diagnosticMessages": true } -------------------------------------------------------------------------------- /DockerComposeFixture.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31005.135 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DockerComposeFixture", "DockerComposeFixture\DockerComposeFixture.csproj", "{1268D2BA-2DB1-49CC-9C69-B9FE6C3FD959}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DockerComposeFixture.Tests", "DockerComposeFixture.Tests\DockerComposeFixture.Tests.csproj", "{6FA8890B-5437-4135-B796-7A818CA5DBB6}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{2CE05DD1-0760-44A3-AAD3-7A8D133B5244}" 11 | ProjectSection(SolutionItems) = preProject 12 | .github\workflows\dotnet.yml = .github\workflows\dotnet.yml 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {1268D2BA-2DB1-49CC-9C69-B9FE6C3FD959}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {1268D2BA-2DB1-49CC-9C69-B9FE6C3FD959}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {1268D2BA-2DB1-49CC-9C69-B9FE6C3FD959}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {1268D2BA-2DB1-49CC-9C69-B9FE6C3FD959}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {6FA8890B-5437-4135-B796-7A818CA5DBB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {6FA8890B-5437-4135-B796-7A818CA5DBB6}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {6FA8890B-5437-4135-B796-7A818CA5DBB6}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {6FA8890B-5437-4135-B796-7A818CA5DBB6}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {13159AB4-72ED-459D-A093-8CE98F011362} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /DockerComposeFixture/Compose/DockerCompose.cs: -------------------------------------------------------------------------------- 1 | using DockerComposeFixture.Logging; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading.Tasks; 5 | 6 | namespace DockerComposeFixture.Compose 7 | { 8 | public class DockerCompose : IDockerCompose 9 | { 10 | private string dockerComposeArgs, dockerComposeUpArgs, dockerComposeDownArgs; 11 | 12 | public DockerCompose(ILogger[] logger) 13 | { 14 | this.Logger = logger; 15 | } 16 | 17 | public void Init(string dockerComposeArgs, string dockerComposeUpArgs, string dockerComposeDownArgs) 18 | { 19 | this.dockerComposeArgs = dockerComposeArgs; 20 | this.dockerComposeUpArgs = dockerComposeUpArgs; 21 | this.dockerComposeDownArgs = dockerComposeDownArgs; 22 | } 23 | 24 | public Task Up() 25 | { 26 | var start = new ProcessStartInfo("docker", $"compose {this.dockerComposeArgs} up {this.dockerComposeUpArgs}"); 27 | return Task.Run(() => this.RunProcess(start) ); 28 | } 29 | 30 | private void RunProcess(ProcessStartInfo processStartInfo) 31 | { 32 | var runner = new ProcessRunner(processStartInfo); 33 | foreach (var logger in this.Logger) 34 | { 35 | runner.Subscribe(logger); 36 | } 37 | runner.Execute(); 38 | } 39 | 40 | public int PauseMs => 1000; 41 | public ILogger[] Logger { get; } 42 | 43 | public void Down() 44 | { 45 | var down = new ProcessStartInfo("docker", $"compose {this.dockerComposeArgs} down {this.dockerComposeDownArgs}"); 46 | this.RunProcess(down); 47 | } 48 | 49 | public IEnumerable Ps() 50 | { 51 | var ps = new ProcessStartInfo("docker", $"compose {this.dockerComposeArgs} ps"); 52 | var runner = new ProcessRunner(ps); 53 | var observerToQueue = new ObserverToQueue(); 54 | 55 | foreach (var logger in this.Logger) 56 | { 57 | runner.Subscribe(logger); 58 | } 59 | runner.Subscribe(observerToQueue); 60 | runner.Execute(); 61 | return observerToQueue.Queue.ToArray(); 62 | } 63 | 64 | public IEnumerable PsWithJsonFormat() 65 | { 66 | var ps = new ProcessStartInfo("docker", $"compose {this.dockerComposeArgs} ps --format json"); 67 | var runner = new ProcessRunner(ps); 68 | var observerToQueue = new ObserverToQueue(); 69 | 70 | foreach (var logger in this.Logger) 71 | { 72 | runner.Subscribe(logger); 73 | } 74 | runner.Subscribe(observerToQueue); 75 | runner.Execute(); 76 | return observerToQueue.Queue.ToArray(); 77 | } 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /DockerComposeFixture/Compose/IDockerCompose.cs: -------------------------------------------------------------------------------- 1 | using DockerComposeFixture.Logging; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace DockerComposeFixture.Compose 6 | { 7 | public interface IDockerCompose 8 | { 9 | void Init(string dockerComposeArgs, string dockerComposeUpArgs, string dockerComposeDownArgs); 10 | void Down(); 11 | IEnumerable Ps(); 12 | IEnumerable PsWithJsonFormat(); 13 | Task Up(); 14 | int PauseMs { get; } 15 | ILogger[] Logger { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DockerComposeFixture/Compose/ObserverToQueue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DockerComposeFixture.Compose 5 | { 6 | public class ObserverToQueue : IObserver 7 | { 8 | public ObserverToQueue() 9 | { 10 | this.Queue = new Queue(); 11 | } 12 | 13 | public Queue Queue { get; set; } 14 | 15 | public void OnCompleted() 16 | { 17 | } 18 | 19 | public void OnError(Exception error) 20 | { 21 | throw error; 22 | } 23 | 24 | public void OnNext(T value) 25 | { 26 | this.Queue.Enqueue(value); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /DockerComposeFixture/Compose/ProcessRunner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace DockerComposeFixture.Compose 6 | { 7 | public class ProcessRunner : IObservable 8 | { 9 | private readonly List> observers; 10 | private readonly ProcessStartInfo startInfo; 11 | 12 | public ProcessRunner(ProcessStartInfo processStartInfo) 13 | { 14 | this.observers = new List>(); 15 | this.startInfo = processStartInfo; 16 | } 17 | 18 | public void Execute() 19 | { 20 | void LogData(object sender, DataReceivedEventArgs e) 21 | { 22 | if (e.Data != null) 23 | { 24 | this.observers.ForEach(o => o.OnNext(e.Data)); 25 | } 26 | } 27 | 28 | var process = new Process { StartInfo = this.startInfo }; 29 | process.StartInfo.RedirectStandardError = true; 30 | process.StartInfo.RedirectStandardOutput = true; 31 | process.StartInfo.UseShellExecute = false; 32 | process.EnableRaisingEvents = true; 33 | 34 | process.OutputDataReceived += LogData; 35 | process.ErrorDataReceived += LogData; 36 | 37 | process.Start(); 38 | process.BeginOutputReadLine(); 39 | process.BeginErrorReadLine(); 40 | process.WaitForExit(); 41 | process.CancelOutputRead(); 42 | 43 | this.observers.ForEach(o => o.OnCompleted()); 44 | } 45 | 46 | public IDisposable Subscribe(IObserver observer) 47 | { 48 | if (!this.observers.Contains(observer)) 49 | { 50 | this.observers.Add(observer); 51 | } 52 | 53 | return new Unsubscriber(this.observers, observer); 54 | } 55 | 56 | private class Unsubscriber : IDisposable 57 | { 58 | private readonly List> observers; 59 | private readonly IObserver observer; 60 | 61 | public Unsubscriber(List> observers, IObserver observer) 62 | { 63 | this.observers = observers; 64 | this.observer = observer; 65 | } 66 | 67 | public void Dispose() 68 | { 69 | if (this.observer != null && this.observers.Contains(this.observer)) 70 | this.observers.Remove(this.observer); 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /DockerComposeFixture/DockerComposeFixture.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 1.2.2 6 | false 7 | Joe Shearn 8 | Docker Compose Fixture 9 | This xUnit fixture will start up a dockerised application and allow you to run integration tests against it. 10 | Joe Shearn 11 | Joe Shearn 12 | https://github.com/devjoes/DockerComposeFixture 13 | https://github.com/devjoes/DockerComposeFixture/blob/master/LICENSE 14 | https://github.com/devjoes/DockerComposeFixture 15 | docker docker-compose xunit 16 | Do not throw null reference if fixture is disposed of without being initialized. 17 | 1.2.2.0 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DockerComposeFixture/DockerComposeFixture.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dockercomposefixture 5 | docker-compose Fixture 6 | 1.2.2 7 | Joe Shearn 8 | Joe Shearn 9 | false 10 | A XUnit fixture that allows you to spin up docker compose files and then run tests against them. 11 | A XUnit fixture that allows you to spin up docker compose files and then run tests against them. 12 | Copyright 2024 13 | XUnit Docker Compose 14 | https://github.com/devjoes/DockerComposeFixture 15 | https://github.com/devjoes/DockerComposeFixture/blob/master/LICENSE 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /DockerComposeFixture/DockerFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using DockerComposeFixture.Compose; 10 | using DockerComposeFixture.Exceptions; 11 | using DockerComposeFixture.Logging; 12 | using Xunit.Abstractions; 13 | 14 | namespace DockerComposeFixture 15 | { 16 | public class DockerFixture : IDisposable 17 | { 18 | private IDockerCompose dockerCompose; 19 | private Func customUpTest; 20 | private bool initialised; 21 | private ILogger[] loggers; 22 | private int startupTimeoutSecs; 23 | private readonly IMessageSink output; 24 | 25 | public DockerFixture(IMessageSink output) 26 | { 27 | this.output = output; 28 | } 29 | 30 | /// 31 | /// Initialize docker compose services from file(s) but only once. 32 | /// If you call this multiple times on the same DockerFixture then it will be ignored. 33 | /// 34 | /// Options that control how docker-compose is executed. 35 | public void InitOnce(Func setupOptions) 36 | { 37 | InitOnce(setupOptions, null); 38 | } 39 | 40 | /// 41 | /// Initialize docker compose services from file(s) but only once. 42 | /// If you call this multiple times on the same DockerFixture then it will be ignored. 43 | /// 44 | /// Options that control how docker-compose is executed. 45 | /// 46 | public void InitOnce(Func setupOptions, IDockerCompose dockerCompose) 47 | { 48 | if (!this.initialised) 49 | { 50 | this.Init(setupOptions, dockerCompose); 51 | this.initialised = true; 52 | } 53 | } 54 | 55 | 56 | /// 57 | /// Initialize docker compose services from file(s). 58 | /// 59 | /// Options that control how docker-compose is executed 60 | public void Init(Func setupOptions) 61 | { 62 | Init(setupOptions, null); 63 | } 64 | 65 | /// 66 | /// Initialize docker compose services from file(s). 67 | /// 68 | /// Options that control how docker-compose is executed 69 | /// 70 | public void Init(Func setupOptions, IDockerCompose compose) 71 | { 72 | var options = setupOptions(); 73 | options.Validate(); 74 | string logFile = options.DebugLog 75 | ? Path.Combine(Path.GetTempPath(), $"docker-compose-{DateTime.Now.Ticks}.log") 76 | : null; 77 | 78 | this.Init(options.DockerComposeFiles, options.DockerComposeUpArgs, options.DockerComposeDownArgs, 79 | options.StartupTimeoutSecs, options.CustomUpTest, compose, this.GetLoggers(logFile).ToArray()); 80 | } 81 | 82 | private IEnumerable GetLoggers(string file) 83 | { 84 | yield return new ListLogger(); 85 | yield return new ConsoleLogger(); 86 | if (this.output != null) 87 | { 88 | yield return new XUnitLogger(this.output); 89 | } 90 | if (!string.IsNullOrEmpty(file)) 91 | { 92 | yield return new FileLogger(file); 93 | } 94 | } 95 | 96 | /// 97 | /// Initialize docker compose services from file(s). 98 | /// 99 | /// Array of docker compose files 100 | /// Arguments to append after 'docker-compose -f file.yml up' 101 | /// Arguments to append after 'docker-compose -f file.yml down' 102 | /// How long to wait for the application to start before giving up 103 | /// Checks whether the docker-compose services have come up correctly based upon the output of docker-compose 104 | /// 105 | /// 106 | public void Init(string[] dockerComposeFiles, string dockerComposeUpArgs, string dockerComposeDownArgs, 107 | int startupTimeoutSecs, Func customUpTest = null, 108 | IDockerCompose dockerCompose = null, ILogger[] logger = null) 109 | { 110 | this.loggers = logger ?? GetLoggers(null).ToArray(); 111 | 112 | var dockerComposeFilePaths = dockerComposeFiles.Select(this.GetComposeFilePath); 113 | this.dockerCompose = dockerCompose ?? new DockerCompose(this.loggers); 114 | this.customUpTest = customUpTest; 115 | this.startupTimeoutSecs = startupTimeoutSecs; 116 | 117 | this.dockerCompose.Init( 118 | string.Join(" ", 119 | dockerComposeFilePaths 120 | .Select(f => $"-f \"{f}\"")) 121 | .Trim(), dockerComposeUpArgs, dockerComposeDownArgs); 122 | this.Start(); 123 | } 124 | 125 | private string GetComposeFilePath(string file) 126 | { 127 | if (File.Exists(file)) 128 | { 129 | return file; 130 | } 131 | 132 | DirectoryInfo curDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); 133 | if (File.Exists(Path.Combine(curDir.FullName, file))) 134 | { 135 | return Path.Combine(curDir.FullName, file); 136 | } 137 | 138 | if (!file.Contains(Path.DirectorySeparatorChar)) 139 | { 140 | while (curDir != null) 141 | { 142 | string curFile = Path.Combine(curDir.FullName, file); 143 | if (File.Exists(curFile)) 144 | { 145 | return curFile; 146 | } 147 | curDir = curDir.Parent; 148 | } 149 | } 150 | throw new ArgumentException($"The file {file} was not found in {AppDomain.CurrentDomain.BaseDirectory} or any parent directories"); 151 | } 152 | 153 | /// 154 | /// Kills all running docker containers if their name contains applicationName 155 | /// 156 | /// Name to match against 157 | /// Optionally kill all docker containers 158 | /// 159 | public static async Task Kill(string applicationName, bool killEverything = false) 160 | { 161 | await Kill(new Regex(Regex.Escape(applicationName)), killEverything); 162 | } 163 | 164 | /// 165 | /// Kills all running docker containers if their name matches a regex 166 | /// 167 | /// Regex to match against 168 | /// Optionally kill all docker containers 169 | /// 170 | public static async Task Kill(Regex filterRx, bool killEverything = false) 171 | { 172 | Process ps = Process.Start(new ProcessStartInfo("docker", "ps") 173 | { 174 | UseShellExecute = false, 175 | RedirectStandardOutput = true 176 | }); 177 | ps.WaitForExit(); 178 | 179 | var ids = (await ps.StandardOutput.ReadToEndAsync()) 180 | .Split('\n') 181 | .Skip(1) 182 | .Where(s => killEverything || filterRx.IsMatch(s)) 183 | .Select(s => Regex.Match(s, @"^[\da-f]+").Value) 184 | .Where(s => !string.IsNullOrEmpty(s)); 185 | 186 | foreach (var id in ids) 187 | { 188 | Process.Start("docker", $"kill {id}").WaitForExit(); 189 | } 190 | 191 | } 192 | 193 | public virtual void Dispose() 194 | { 195 | this.Stop(); 196 | } 197 | 198 | private void Start() 199 | { 200 | if (this.CheckIfRunning().hasContainers) 201 | { 202 | this.loggers.Log("---- stopping already running docker services ----"); 203 | this.Stop(); 204 | } 205 | 206 | this.loggers.Log("---- starting docker services ----"); 207 | var upTask = this.dockerCompose.Up(); 208 | 209 | for (int i = 0; i < this.startupTimeoutSecs; i++) 210 | { 211 | if (upTask.IsCompleted) 212 | { 213 | this.loggers.Log("docker-compose exited prematurely"); 214 | break; 215 | } 216 | this.loggers.Log($"---- checking docker services ({i + 1}/{this.startupTimeoutSecs}) ----"); 217 | Thread.Sleep(this.dockerCompose.PauseMs); 218 | if (this.customUpTest != null) 219 | { 220 | if (this.customUpTest(this.loggers.GetLoggedLines())) 221 | { 222 | this.loggers.Log("---- custom up test satisfied ----"); 223 | return; 224 | } 225 | } 226 | else 227 | { 228 | var (hasContainers, containersAreUp) = this.CheckIfRunning(); 229 | if (hasContainers && containersAreUp) 230 | { 231 | this.loggers.Log("---- docker services are up ----"); 232 | return; 233 | } 234 | } 235 | } 236 | throw new DockerComposeException(this.loggers.GetLoggedLines()); 237 | } 238 | 239 | private (bool hasContainers, bool containersAreUp) CheckIfRunning() 240 | { 241 | var lines = dockerCompose.PsWithJsonFormat() 242 | .Where(l => l != null && l.StartsWith("{")) 243 | .ToList(); 244 | return ( 245 | lines.Any(), 246 | lines.Count(l => l.Contains("\"Status\": \"Up")) == lines.Count); 247 | } 248 | 249 | private void Stop() 250 | { 251 | if (this.dockerCompose != null) 252 | { 253 | this.dockerCompose.Down(); 254 | } 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /DockerComposeFixture/DockerFixtureOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DockerComposeFixture 4 | { 5 | /// 6 | /// Options that control how docker-compose is executed 7 | /// 8 | public class DockerFixtureOptions : IDockerFixtureOptions 9 | { 10 | /// 11 | /// Checks whether the docker-compose services have come up correctly based upon the output of docker-compose 12 | /// 13 | public Func CustomUpTest { get; set; } 14 | 15 | /// 16 | /// Array of docker compose files 17 | /// Files are converted into the arguments '-f file1 -f file2 etc' 18 | /// Default is 'docker-compose.yml' 19 | /// 20 | public string[] DockerComposeFiles { get; set; } = new[] { "docker-compose.yml" }; 21 | /// 22 | /// When true this logs docker-compose output to %temp%\docker-compose-*.log 23 | /// 24 | public bool DebugLog { get; set; } 25 | /// 26 | /// Arguments to append after 'docker-compose -f file.yml up' 27 | /// Default is 'docker-compose -f file.yml up' you can append '--build' if you want it to always build 28 | /// 29 | public string DockerComposeUpArgs { get; set; } = ""; 30 | /// 31 | /// Arguments to append after 'docker-compose -f file.yml down' 32 | /// Default is 'docker-compose -f file.yml down --remove-orphans' you can add '--rmi all' if you want to guarantee a fresh build on each test 33 | /// 34 | public string DockerComposeDownArgs { get; set; } = "--remove-orphans"; 35 | 36 | /// 37 | /// How many seconds to wait for the application to start before giving up. (Default is 120.) 38 | /// 39 | public int StartupTimeoutSecs { get; set; } = 120; 40 | 41 | public void Validate() 42 | { 43 | if (this.StartupTimeoutSecs < 1) 44 | { 45 | throw new ArgumentException(nameof(this.StartupTimeoutSecs)); 46 | } 47 | if (this.DockerComposeFiles == null 48 | || this.DockerComposeFiles.Length == 0) 49 | { 50 | throw new ArgumentException(nameof(this.DockerComposeFiles)); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /DockerComposeFixture/Exceptions/DockerComposeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DockerComposeFixture.Exceptions 4 | { 5 | public class DockerComposeException:Exception 6 | { 7 | public DockerComposeException(string[] loggedLines):base($"docker-compose failed - see {nameof(DockerComposeOutput)} property") 8 | { 9 | this.DockerComposeOutput = loggedLines; 10 | } 11 | 12 | public string[] DockerComposeOutput { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DockerComposeFixture/IDockerFixtureOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DockerComposeFixture 4 | { 5 | public interface IDockerFixtureOptions 6 | { 7 | Func CustomUpTest { get; set; } 8 | string[] DockerComposeFiles { get; set; } 9 | bool DebugLog { get; set; } 10 | string DockerComposeUpArgs { get; set; } 11 | string DockerComposeDownArgs { get; set; } 12 | int StartupTimeoutSecs { get; set; } 13 | 14 | void Validate(); 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /DockerComposeFixture/Logging/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace DockerComposeFixture.Logging 5 | { 6 | public class ConsoleLogger : ILogger 7 | { 8 | public void OnCompleted() 9 | { 10 | 11 | } 12 | 13 | public void OnError(Exception error) 14 | { 15 | this.Log(error.Message + "\n" + error.StackTrace); 16 | throw error; 17 | } 18 | 19 | public void OnNext(string value) 20 | { 21 | this.Log(value); 22 | } 23 | 24 | public void Log(string msg) 25 | { 26 | Debug.WriteLine(msg); 27 | Console.WriteLine(msg); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /DockerComposeFixture/Logging/FileLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace DockerComposeFixture.Logging 5 | { 6 | public class FileLogger : ILogger 7 | { 8 | private readonly string logfileName; 9 | 10 | public FileLogger(string logfileName) 11 | { 12 | if (logfileName != null) 13 | { 14 | if (File.Exists(logfileName)) 15 | { 16 | File.Delete(logfileName); 17 | } 18 | } 19 | this.logfileName = logfileName; 20 | } 21 | 22 | public void OnCompleted() 23 | { 24 | 25 | } 26 | 27 | public void OnError(Exception error) 28 | { 29 | this.Log(error.Message + "\n" + error.StackTrace); 30 | throw error; 31 | } 32 | 33 | public void OnNext(string value) 34 | { 35 | this.Log(value); 36 | } 37 | 38 | public void Log(string msg) 39 | { 40 | using (var stream = new FileStream(this.logfileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) 41 | using (var writer = new StreamWriter(stream)) 42 | { 43 | writer.WriteLine(msg); 44 | writer.Flush(); 45 | writer.Close(); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /DockerComposeFixture/Logging/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DockerComposeFixture.Logging 4 | { 5 | public interface ILogger : IObserver 6 | { 7 | void Log(string msg); 8 | } 9 | } -------------------------------------------------------------------------------- /DockerComposeFixture/Logging/ListLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DockerComposeFixture.Logging 5 | { 6 | public class ListLogger : ILogger 7 | { 8 | public ListLogger() 9 | { 10 | this.LoggedLines = new List(); 11 | } 12 | 13 | public void OnCompleted() 14 | { 15 | 16 | } 17 | public void OnError(Exception error) 18 | { 19 | this.Log(error.Message + "\n" + error.StackTrace); 20 | throw error; 21 | } 22 | 23 | 24 | public void OnNext(string value) 25 | { 26 | this.Log(value); 27 | } 28 | 29 | public void Log(string msg) 30 | { 31 | this.LoggedLines.Add(msg); 32 | } 33 | 34 | public List LoggedLines { get; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /DockerComposeFixture/Logging/LoggingExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace DockerComposeFixture.Logging 4 | { 5 | public static class LoggingExtensionMethods 6 | { 7 | public static void Log(this ILogger[] loggers, string msg) 8 | { 9 | foreach (var logger in loggers) 10 | { 11 | logger.Log(msg); 12 | } 13 | } 14 | 15 | public static string[] GetLoggedLines(this ILogger[] loggers) 16 | { 17 | var listLogger = (ListLogger)loggers.Single(l => l is ListLogger); 18 | return listLogger.LoggedLines.ToArray(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /DockerComposeFixture/Logging/XUnitLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit.Abstractions; 3 | using Xunit.Sdk; 4 | 5 | namespace DockerComposeFixture.Logging 6 | { 7 | public class XUnitLogger : ILogger 8 | { 9 | private readonly IMessageSink xlogOutput; 10 | 11 | public XUnitLogger(IMessageSink xlogOutput) 12 | { 13 | this.xlogOutput = xlogOutput; 14 | } 15 | 16 | public void OnCompleted() 17 | { 18 | 19 | } 20 | 21 | public void OnError(Exception error) 22 | { 23 | this.Log(error.Message + "\n" + error.StackTrace); 24 | throw error; 25 | } 26 | 27 | public void OnNext(string value) 28 | { 29 | this.Log(value); 30 | } 31 | 32 | public void Log(string msg) 33 | { 34 | this.xlogOutput.OnMessage(new DiagnosticMessage(msg)); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 devjoes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Compose Fixture 2 | A XUnit fixture that allows you to spin up docker compose files and then run tests against them. 3 | 4 | ## Example Integration Test 5 | 6 | public class IntegrationTests : IClassFixture 7 | { 8 | public IntegrationTests(DockerFixture dockerFixture) 9 | { 10 | dockerFixture.InitOnce(() => new DockerFixtureOptions 11 | { 12 | DockerComposeFiles = new[] { "docker-compose.yml" }, 13 | CustomUpTest = output => output.Any(l => l.Contains("App is ready")) 14 | }); 15 | } 16 | 17 | // Tests go here 18 | } 19 | 20 | ## Logging 21 | To enable XUnit logging you will have to add a xunit.runner.json file to your test project. The file should be copied to the output directory and should look like this: 22 | 23 | { "diagnosticMessages": true } 24 | --------------------------------------------------------------------------------