Best Kotest code snippet using io.kotest.assertions.extracting
SubscriptionManagerTest.kt
Source:SubscriptionManagerTest.kt  
...26import fr.nihilus.music.core.os.PermissionDeniedException27import fr.nihilus.music.core.test.coroutines.CoroutineTestRule28import fr.nihilus.music.core.test.failAssumption29import fr.nihilus.music.service.browser.PaginationOptions30import io.kotest.assertions.extracting31import io.kotest.assertions.throwables.shouldNotThrow32import io.kotest.assertions.throwables.shouldThrow33import io.kotest.matchers.collections.*34import io.kotest.matchers.nulls.shouldNotBeNull35import io.kotest.matchers.shouldBe36import io.kotest.matchers.types.shouldBeSameInstanceAs37import io.kotest.matchers.types.shouldNotBeSameInstanceAs38import kotlinx.coroutines.CoroutineScope39import kotlinx.coroutines.delay40import kotlinx.coroutines.yield41import org.junit.Rule42import org.junit.runner.RunWith43import kotlin.test.BeforeTest44import kotlin.test.Test45@RunWith(AndroidJUnit4::class)46class SubscriptionManagerTest {47    @get:Rule48    val test = CoroutineTestRule()49    private lateinit var dispatchers: AppDispatchers50    @BeforeTest51    fun initDispatchers() {52        dispatchers = AppDispatchers(test.dispatcher)53    }54    @Test55    fun `When loading children, then subscribe to their parent in the tree`() =56        test.runWithin { scope ->57            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)58            val children = manager.loadChildren(MediaId(TYPE_TRACKS, CATEGORY_ALL), null)59            extracting(children, MediaContent::id).shouldContainExactly(60                MediaId(TYPE_TRACKS, CATEGORY_ALL, 161),61                MediaId(TYPE_TRACKS, CATEGORY_ALL, 309),62                MediaId(TYPE_TRACKS, CATEGORY_ALL, 481),63                MediaId(TYPE_TRACKS, CATEGORY_ALL, 48),64                MediaId(TYPE_TRACKS, CATEGORY_ALL, 125),65                MediaId(TYPE_TRACKS, CATEGORY_ALL, 294),66                MediaId(TYPE_TRACKS, CATEGORY_ALL, 219),67                MediaId(TYPE_TRACKS, CATEGORY_ALL, 75),68                MediaId(TYPE_TRACKS, CATEGORY_ALL, 464),69                MediaId(TYPE_TRACKS, CATEGORY_ALL, 477)70            )71        }72    @Test73    fun `Given active subscription, when reloading then return cached children`() =74        test.runWithin { scope ->75            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)76            val parentId = MediaId(TYPE_TRACKS, CATEGORY_ALL)77            // Trigger initial subscription78            val children = manager.loadChildren(parentId, null)79            // Reload children, and check that those are the same80            val reloadedChildren = manager.loadChildren(parentId, null)81            reloadedChildren shouldBeSameInstanceAs children82        }83    @Test84    fun `When loading children of invalid parent, then fail with NoSuchElementException`() =85        test.runWithin { scope ->86            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)87            shouldThrow<NoSuchElementException> {88                val invalidMediaId = MediaId(TYPE_PLAYLISTS, "unknown")89                manager.loadChildren(invalidMediaId, null)90            }91        }92    @Test93    fun `Given denied permission, when loading children then fail with PermissionDeniedException`() =94        test.runWithin { scope ->95            val deniedTree = PermissionBrowserTree(granted = false)96            val manager = CachingSubscriptionManager(scope, deniedTree, dispatchers)97            val permissionFailure = shouldThrow<PermissionDeniedException> {98                val parentId = MediaId(TYPE_TRACKS, CATEGORY_ALL)99                manager.loadChildren(parentId, null)100            }101            permissionFailure.permission shouldBe Manifest.permission.READ_EXTERNAL_STORAGE102        }103    @Test104    fun `After permission grant, when loading children then proceed without error`() =105        test.runWithin { scope ->106            val permissionTree = PermissionBrowserTree(granted = false)107            val manager = CachingSubscriptionManager(scope, permissionTree, dispatchers)108            val parentId = MediaId(TYPE_TRACKS, CATEGORY_ALL)109            shouldThrow<PermissionDeniedException> {110                manager.loadChildren(parentId, null)111            }112            permissionTree.granted = true113            shouldNotThrow<PermissionDeniedException> {114                manager.loadChildren(parentId, null)115            }116        }117    @Test118    fun `After permission denial, when loading children then recover`() = test.runWithin { scope ->119        val permissionTree = PermissionBrowserTree(granted = true)120        val manager = CachingSubscriptionManager(scope, permissionTree, dispatchers)121        // Start initial subscription.122        val parentId = MediaId(TYPE_TRACKS, CATEGORY_ALL)123        shouldNotThrow<PermissionDeniedException> {124            manager.loadChildren(parentId, null)125        }126        // Then the permission is denied. When trying to update children, it should fail.127        permissionTree.granted = false128        delay(1001)129        shouldThrow<PermissionDeniedException> {130            manager.loadChildren(parentId, null)131        }132        // It should create a new subscription and succeed.133        permissionTree.granted = true134        shouldNotThrow<PermissionDeniedException> {135            manager.loadChildren(parentId, null)136        }137    }138    @Test139    fun `Given max subscriptions, when loading children then dispose oldest subscriptions`() =140        test.runWithin { scope ->141            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)142            // Trigger subscription of albums 0 to MAX included.143            val childrenPerAlbumId = (0..MAX_ACTIVE_SUBSCRIPTIONS).map { albumId ->144                val parentId = MediaId(TYPE_ALBUMS, albumId.toString())145                manager.loadChildren(parentId, null)146            }147            // Subscription to album 0 should have been disposed when subscribing to album MAX.148            // Its children should not be loaded from cache.149            val albumZeroChildren = manager.loadChildren(MediaId(TYPE_ALBUMS, "0"), null)150            albumZeroChildren shouldNotBeSameInstanceAs childrenPerAlbumId[0]151            // Previous re-subscription to album 0 should clear subscription to album 1,152            // and therefore it should not load its children from cache.153            val albumOneChildren = manager.loadChildren(MediaId(TYPE_ALBUMS, "1"), null)154            albumOneChildren shouldNotBeSameInstanceAs childrenPerAlbumId[1]155            // Subscription to album MAX should still be active,156            // hence children are loaded from cache.157            val lastAlbumId = MediaId(TYPE_ALBUMS, MAX_ACTIVE_SUBSCRIPTIONS.toString())158            val albumMaxChildren = manager.loadChildren(lastAlbumId, null)159            albumMaxChildren shouldBeSameInstanceAs childrenPerAlbumId[MAX_ACTIVE_SUBSCRIPTIONS]160        }161    @Test162    fun `Given max subscriptions, when reloading children then keep its subscription longer`() =163        test.runWithin { scope ->164            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)165            // Trigger subscriptions to reach the max allowed count.166            val childrenPerAlbumId = (0 until MAX_ACTIVE_SUBSCRIPTIONS).map { albumId ->167                val parentId = MediaId(TYPE_ALBUMS, albumId.toString())168                manager.loadChildren(parentId, null)169            }170            // Reload children of album 0, then create a new subscription.171            manager.loadChildren(MediaId(TYPE_ALBUMS, "0"), null)172            manager.loadChildren(MediaId(TYPE_ALBUMS, MAX_ACTIVE_SUBSCRIPTIONS.toString()), null)173            // If album 0 had not been reloaded, its subscription should have been disposed.174            // The oldest subscription now being that of album 1, it has been disposed instead.175            val albumZeroChildren = manager.loadChildren(MediaId(TYPE_ALBUMS, "0"), null)176            val albumOneChildren = manager.loadChildren(MediaId(TYPE_ALBUMS, "1"), null)177            albumZeroChildren shouldBeSameInstanceAs childrenPerAlbumId[0]178            albumOneChildren shouldNotBeSameInstanceAs childrenPerAlbumId[1]179        }180    @Test181    fun `Given pages of size N, when loading children then return the N first items`() =182        test.runWithin { scope ->183            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)184            val paginatedChildren = manager.loadChildren(185                MediaId(TYPE_TRACKS, CATEGORY_ALL),186                PaginationOptions(0, 3)187            )188            extracting(paginatedChildren, MediaContent::id).shouldContainExactly(189                MediaId(TYPE_TRACKS, CATEGORY_ALL, 161),190                MediaId(TYPE_TRACKS, CATEGORY_ALL, 309),191                MediaId(TYPE_TRACKS, CATEGORY_ALL, 481)192            )193        }194    @Test195    fun `Given the page X of size N, when loading children then return N items from position NX`() =196        test.runWithin { scope ->197            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)198            val paginatedChildren = manager.loadChildren(199                MediaId(TYPE_TRACKS, CATEGORY_ALL),200                PaginationOptions(3, 2)201            )202            extracting(paginatedChildren, MediaContent::id).shouldContainExactly(203                MediaId(TYPE_TRACKS, CATEGORY_ALL, 219),204                MediaId(TYPE_TRACKS, CATEGORY_ALL, 75)205            )206        }207    @Test208    fun `Given a page after the last page, when loading children then return no children`() =209        test.runWithin { scope ->210            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)211            val pagePastChildren = manager.loadChildren(212                MediaId(TYPE_TRACKS, CATEGORY_ALL),213                PaginationOptions(2, 5)214            )215            pagePastChildren.shouldBeEmpty()216        }...BrowserTreeSearchTest.kt
Source:BrowserTreeSearchTest.kt  
...26import fr.nihilus.music.media.provider.MediaDao27import fr.nihilus.music.media.provider.Track28import fr.nihilus.music.media.usage.UsageManager29import fr.nihilus.music.service.MediaContent30import io.kotest.assertions.extracting31import io.kotest.matchers.collections.shouldBeEmpty32import io.kotest.matchers.collections.shouldContainAll33import io.kotest.matchers.collections.shouldContainExactly34import kotlinx.coroutines.test.runTest35import org.junit.runner.RunWith36import kotlin.test.Test37@RunWith(AndroidJUnit4::class)38internal class BrowserTreeSearchTest {39    private val context: Context40        get() = ApplicationProvider.getApplicationContext()41    @Test42    fun `When searching with an empty query then return no results`() = runTest {43        val tree = givenRealisticBrowserTree()44        val results = tree.search(SearchQuery.Empty)45        results.shouldBeEmpty()46    }47    @Test48    fun `Given artist focus, when searching an artist then return that artist`() = runTest {49        val browserTree = givenRealisticBrowserTree()50        val results = browserTree.search(SearchQuery.Artist("Foo Fighters"))51        extracting(results, MediaContent::id).shouldContainExactly(52            MediaId(TYPE_ARTISTS, "13")53        )54    }55    @Test56    fun `Given album focus, when searching an album then return that album`() = runTest {57        val tree = givenRealisticBrowserTree()58        val results = tree.search(SearchQuery.Album("Foo Fighters", "Wasting Light"))59        extracting(results, MediaContent::id).shouldContainExactly(60            MediaId(TYPE_ALBUMS, "26")61        )62    }63    @Test64    fun `Given track focused query, when searching songs then return that song`() = runTest {65        val tree = givenRealisticBrowserTree()66        val results = tree.search(67            SearchQuery.Song(68                artist = "Foo Fighters",69                album = "Concrete and Gold",70                title = "Dirty Water"71            )72        )73        extracting(results, MediaContent::id).shouldContainExactly(74            MediaId(TYPE_TRACKS, CATEGORY_ALL, 481)75        )76    }77    @Test78    fun `Given exact artist name, when searching unfocused then return that artist`() = runTest {79        val tree = givenRealisticBrowserTree()80        val results = tree.search(SearchQuery.Unspecified("foo fighters"))81        extracting(results, MediaContent::id).shouldContainExactly(82            MediaId(TYPE_ARTISTS, "13")83        )84    }85    @Test86    fun `Given exact album title, when searching unfocused then return that album`() = runTest {87        val tree = givenRealisticBrowserTree()88        val results = tree.search(SearchQuery.Unspecified("concrete and gold"))89        extracting(results, MediaContent::id).shouldContainExactly(90            MediaId(TYPE_ALBUMS, "102")91        )92    }93    @Test94    fun `Given exact song title, when searching unfocused then return that song`() = runTest {95        val tree = givenRealisticBrowserTree()96        val results = tree.search(SearchQuery.Unspecified("dirty water"))97        extracting(results, MediaContent::id).shouldContainExactly(98            MediaId(TYPE_TRACKS, CATEGORY_ALL, 481)99        )100    }101    @Test102    fun `Given query matching both album and song, when searching albums then return only that album`() =103        runTest {104            val tree = givenRealisticBrowserTree()105            val results = tree.search(SearchQuery.Album("Avenged Sevenfold", "Nightmare"))106            extracting(results, MediaContent::id).shouldContainExactly(107                MediaId(TYPE_ALBUMS, "6")108            )109        }110    @Test111    fun `Given query matching both album and song, when searching unfocused then return both`() =112        runTest {113            val tree = givenRealisticBrowserTree()114            // Both the album "Nightmare" and its eponymous track are listed in search results.115            // Note that the album should be listed first.116            val results = tree.search(SearchQuery.Unspecified("nightmare"))117            extracting(results, MediaContent::id).shouldContainExactly(118                MediaId(TYPE_ALBUMS, "6"),119                MediaId(TYPE_TRACKS, CATEGORY_ALL, 75)120            )121        }122    @Test123    fun `Given uppercase query, when searching unfocused then return results`() = runTest {124        val tree = givenRealisticBrowserTree()125        val results = tree.search(SearchQuery.Unspecified("Nightmare"))126        extracting(results, MediaContent::id).shouldContainAll(127            MediaId(TYPE_ALBUMS, "6"),128            MediaId(TYPE_TRACKS, CATEGORY_ALL, 75)129        )130    }131    @Test132    fun `Given pattern query, when searching then return items containing that pattern`() =133        runTest {134            val tracks = listOf(135                Track(136                    23,137                    "Another Brick In The Wall",138                    "Pink Floyd",139                    "The Wall",140                    0,141                    1,142                    5,143                    "",144                    null,145                    0,146                    2,147                    2,148                    0149                ),150                Track(151                    34,152                    "Another One Bites the Dust",153                    "Queen",154                    "The Game",155                    0,156                    1,157                    3,158                    "",159                    null,160                    0,161                    3,162                    3,163                    0164                ),165                Track(166                    56,167                    "Nothing Else Matters",168                    "Metallica",169                    "Metallica",170                    0L,171                    1,172                    8,173                    "",174                    null,175                    0,176                    4,177                    4,178                    0179                ),180                Track(181                    12,182                    "Otherside",183                    "Red Hot Chili Peppers",184                    "Californication",185                    0,186                    1,187                    6,188                    "",189                    null,190                    0,191                    1,192                    1,193                    0194                ),195                Track(196                    98,197                    "You've Got Another Thing Comin",198                    "Judas Priest",199                    "Screaming for Vengeance",200                    0,201                    1,202                    8,203                    "",204                    null,205                    0,206                    7,207                    7,208                    0209                )210            )211            val tree = BrowserTree(TestMediaDao(emptyList(), emptyList(), tracks))212            // "OTHERside" is listed first (it starts with the pattern),213            // then "AnOTHER Brick In the Wall" (same pattern at same position),214            // then "AnOTHER One Bites the Dust" (one word contains the pattern but slightly longer),215            // then "You've Got AnOTHER Thing Comin" (pattern matches farther)216            val results = tree.search(SearchQuery.Unspecified("other"))217            extracting(results, MediaContent::id).shouldContainExactly(218                MediaId(TYPE_TRACKS, CATEGORY_ALL, 12),219                MediaId(TYPE_TRACKS, CATEGORY_ALL, 23),220                MediaId(TYPE_TRACKS, CATEGORY_ALL, 34),221                MediaId(TYPE_TRACKS, CATEGORY_ALL, 98)222            )223    }224    @Test225    fun `Given pattern query that matches multiple items equally, when searching then return shortest first`() =226        runTest {227            val tracks = listOf(228                Track(229                    10,230                    "Are You Ready",231                    "AC/DC",232                    "The Razor's Edge",233                    0,234                    1,235                    7,236                    "",237                    null,238                    0,239                    32,240                    18,241                    0242                ),243                Track(244                    42,245                    "Are You Gonna Be My Girl",246                    "Jet",247                    "Get Born",248                    0,249                    1,250                    2,251                    "",252                    null,253                    0,254                    78,255                    90,256                    0257                ),258                Track(259                    63,260                    "Are You Gonna Go My Way",261                    "Lenny Kravitz",262                    "Are You Gonna Go My Way",263                    0,264                    1,265                    1,266                    "",267                    null,268                    0,269                    57,270                    23,271                    0272                )273            )274            val tree = BrowserTree(TestMediaDao(emptyList(), emptyList(), tracks))275            // When the pattern matches multiple items equally,276            // shorter items should be displayed first.277            val results = tree.search(SearchQuery.Unspecified("are"))278            extracting(results, MediaContent::id).shouldContainExactly(279                MediaId(TYPE_TRACKS, CATEGORY_ALL, 10),280                MediaId(TYPE_TRACKS, CATEGORY_ALL, 63),281                MediaId(TYPE_TRACKS, CATEGORY_ALL, 42)282            )283    }284    @Test285    fun `When search pattern matches multiple items, then first return results that matches the start of a word`() =286        runTest {287            val tracks = listOf(288                Track(90, "Avalanche", "Ghost", "Prequelle", 0, 1, 12, "", null, 0, 56, 97, 0),289                Track(290                    91,291                    "No Grave But The Sea",292                    "Alestorm",293                    "No Grave But The Sea",294                    0,295                    1,296                    1,297                    "",298                    null,299                    0,300                    456,301                    856,302                    0303                ),304                Track(305                    356,306                    "Gravity",307                    "Bullet For My Valentine",308                    "Gravity",309                    0,310                    1,311                    8,312                    "",313                    null,314                    0,315                    45,316                    99,317                    0318                )319            )320            val artist = listOf(321                Artist(65, "Avatar", 0, 0, null),322                Artist(98, "Avenged Sevenfold", 0, 0, null)323            )324            val tree = BrowserTree(TestMediaDao(artist, emptyList(), tracks))325            val results = tree.search(SearchQuery.Unspecified("av"))326            extracting(results, MediaContent::id).shouldContainExactly(327                MediaId(TYPE_ARTISTS, "65"), // AVatar328                MediaId(TYPE_TRACKS, CATEGORY_ALL, 90), // AValanche329                MediaId(TYPE_ARTISTS, "98"), // AVenged Sevenfold330                MediaId(TYPE_TRACKS, CATEGORY_ALL, 356), // GrAVity331                MediaId(TYPE_TRACKS, CATEGORY_ALL, 91) // No GrAVe But the Sea332            )333    }334    private fun BrowserTree(335        mediaDao: MediaDao,336        usageManager: UsageManager = StubUsageManager337    ): BrowserTree = BrowserTreeImpl(context, mediaDao, StubPlaylistDao, usageManager, StubSpotifyManager)338    private fun givenRealisticBrowserTree(): BrowserTreeImpl =339        BrowserTreeImpl(context, TestMediaDao(), TestPlaylistDao(), TestUsageManager(), StubSpotifyManager)340}...DockerClientImagePullSpec.kt
Source:DockerClientImagePullSpec.kt  
1/*2    Copyright 2017-2022 Charles Korn.3    Licensed under the Apache License, Version 2.0 (the "License");4    you may not use this file except in compliance with the License.5    You may obtain a copy of the License at6        https://www.apache.org/licenses/LICENSE-2.07    Unless required by applicable law or agreed to in writing, software8    distributed under the License is distributed on an "AS IS" BASIS,9    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.10    See the License for the specific language governing permissions and11    limitations under the License.12*/13package batect.dockerclient14import io.kotest.assertions.throwables.shouldThrow15import io.kotest.core.spec.style.ShouldSpec16import io.kotest.inspectors.forAtLeastOne17import io.kotest.matchers.collections.shouldBeIn18import io.kotest.matchers.collections.shouldContain19import io.kotest.matchers.collections.shouldContainAnyOf20import io.kotest.matchers.collections.shouldEndWith21import io.kotest.matchers.collections.shouldStartWith22import io.kotest.matchers.shouldBe23import io.kotest.matchers.shouldNotBe24class DockerClientImagePullSpec : ShouldSpec({25    val client = closeAfterTest(DockerClient.Builder().build())26    val defaultLinuxTestImage = "gcr.io/distroless/static@sha256:aadea1b1f16af043a34491eec481d0132479382096ea34f608087b4bef3634be"27    val defaultWindowsTestImage = "mcr.microsoft.com/windows/nanoserver@sha256:4f06e1d8263b934d2e88dc1c6ff402f5b499c4d19ad6d0e2a5b9ee945f782928" // This is nanoserver:180928    val testImages = when (testEnvironmentContainerOperatingSystem) {29        ContainerOperatingSystem.Linux -> mapOf(30            "with a digest and no tag" to defaultLinuxTestImage,31            "with a digest and tag" to "gcr.io/distroless/static:063a079c1a87bad3369cb9daf05e371e925c0c91@sha256:aadea1b1f16af043a34491eec481d0132479382096ea34f608087b4bef3634be",32            "with a tag and no digest" to "gcr.io/distroless/static:063a079c1a87bad3369cb9daf05e371e925c0c91",33            "with neither a digest nor a tag" to "gcr.io/distroless/static",34            // To recreate this image:35            //   docker pull gcr.io/distroless/static@sha256:aadea1b1f16af043a34491eec481d0132479382096ea34f608087b4bef3634be36            //   docker tag gcr.io/distroless/static@sha256:aadea1b1f16af043a34491eec481d0132479382096ea34f608087b4bef3634be ghcr.io/batect/docker-client:sample-authenticated-image37            //   docker push ghcr.io/batect/docker-client:sample-authenticated-image38            //39            // If you need to configure credentials locally: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry40            "that requires authentication to pull" to "ghcr.io/batect/docker-client:sample-authenticated-image"41        )42        ContainerOperatingSystem.Windows -> mapOf(43            "with a tag" to defaultWindowsTestImage44        )45    }46    val imageThatDoesNotExist = "batect/this-image-does-not-exist:abc123"47    beforeEach {48        testImages.values.forEach { image -> client.deleteImageIfPresent(image) }49    }50    testImages.forEach { (description, image) ->51        should("be able to pull, get and delete an image $description").onlyIfDockerDaemonPresent {52            val imageReferenceFromPull = client.pullImage(image)53            val imageReferenceFromGet = client.getImage(image)54            imageReferenceFromPull shouldBe imageReferenceFromGet55            client.deleteImage(imageReferenceFromPull, force = true)56            val imageReferenceAfterDelete = client.getImage(image)57            imageReferenceAfterDelete shouldBe null58        }59    }60    context("pulling an image that does not exist on the local machine") {61        // Recreate this image with 'resources/base-images/recreate.sh'.62        val image = "ghcr.io/batect/docker-client:image-pull-progress@sha256:ed32e6eb4f059d2ac57e47413855d737db00c21f39edb0a5845c3a30a18a7263"63        val imageWithoutTag = "ghcr.io/batect/docker-client@sha256:ed32e6eb4f059d2ac57e47413855d737db00c21f39edb0a5845c3a30a18a7263"64        beforeAny {65            client.deleteImageIfPresent(image)66            client.deleteImageIfPresent(imageWithoutTag)67        }68        should("report progress information while pulling an image").onlyIfDockerDaemonSupportsLinuxContainers {69            val progressUpdatesReceived = mutableListOf<ImagePullProgressUpdate>()70            client.pullImage(image) { update ->71                progressUpdatesReceived.add(update)72            }73            val layerId = "0f17b32804d3"74            val layerSize = 127L75            progressUpdatesReceived.forAtLeastOne {76                it.message shouldBe "Pulling from batect/docker-client"77                it.detail shouldBe null78                it.id shouldBeIn setOf(imageWithoutTag, imageWithoutTag.substringAfter('@')) // Older versions of Docker only return the digest here79            }80            progressUpdatesReceived shouldContain ImagePullProgressUpdate("Pulling fs layer", ImagePullProgressDetail(0, 0), layerId)81            // Docker does some rate limiting of progress updates, so to make this test more resilient to this non-determinism, we82            // consider the test passing if at least one of these updates is posted.83            progressUpdatesReceived shouldContainAnyOf setOf(84                ImagePullProgressUpdate("Downloading", ImagePullProgressDetail(0, layerSize), layerId),85                ImagePullProgressUpdate("Downloading", ImagePullProgressDetail(layerSize, layerSize), layerId),86                ImagePullProgressUpdate("Download complete", ImagePullProgressDetail(0, 0), layerId)87            )88            progressUpdatesReceived.forAtLeastOne {89                it.message shouldBe "Extracting"90                it.detail shouldNotBe null91                it.detail!!.total shouldBe layerSize92                it.id shouldBe layerId93            }94            progressUpdatesReceived shouldEndWith listOf(95                ImagePullProgressUpdate("Pull complete", ImagePullProgressDetail(0, 0), layerId),96                ImagePullProgressUpdate("Digest: sha256:ed32e6eb4f059d2ac57e47413855d737db00c21f39edb0a5845c3a30a18a7263", null, ""),97                ImagePullProgressUpdate("Status: Downloaded newer image for $imageWithoutTag", null, "")98            )99        }100    }101    should("gracefully handle a progress callback that throws an exception while pulling an image").onlyIfDockerDaemonSupportsLinuxContainers {102        val exceptionThrownByCallbackHandler = RuntimeException("This is an exception from the callback handler")103        val exceptionThrownByPullMethod = shouldThrow<ImagePullFailedException> {104            client.pullImage(defaultLinuxTestImage) {105                throw exceptionThrownByCallbackHandler106            }107        }108        exceptionThrownByPullMethod.message shouldBe "Image pull progress receiver threw an exception: $exceptionThrownByCallbackHandler"109        exceptionThrownByPullMethod.cause shouldBe exceptionThrownByCallbackHandler110    }111    should("report progress information while pulling a Windows image").onlyIfDockerDaemonSupportsWindowsContainers {112        val image = defaultWindowsTestImage113        val progressUpdatesReceived = mutableListOf<ImagePullProgressUpdate>()114        client.pullImage(image) { update ->115            progressUpdatesReceived.add(update)116        }117        val layerId = "934e212983f2"118        val layerSize = 102661372119        progressUpdatesReceived shouldStartWith listOf(120            ImagePullProgressUpdate("Pulling from windows/nanoserver", null, image),121            ImagePullProgressUpdate("Pulling fs layer", ImagePullProgressDetail(0, 0), layerId),122        )123        progressUpdatesReceived.forAtLeastOne {124            it.message shouldBe "Downloading"125            it.detail shouldNotBe null126            it.detail!!.total shouldBe layerSize127            it.id shouldBe layerId128        }129        progressUpdatesReceived shouldContain ImagePullProgressUpdate("Download complete", ImagePullProgressDetail(0, 0), layerId)130        progressUpdatesReceived.forAtLeastOne {131            it.message shouldBe "Extracting"132            it.detail shouldNotBe null133            it.detail!!.total shouldBe layerSize134            it.id shouldBe layerId135        }136        progressUpdatesReceived shouldEndWith listOf(137            ImagePullProgressUpdate("Pull complete", ImagePullProgressDetail(0, 0), layerId),138            ImagePullProgressUpdate("Digest: sha256:4f06e1d8263b934d2e88dc1c6ff402f5b499c4d19ad6d0e2a5b9ee945f782928", null, ""),139            ImagePullProgressUpdate("Status: Downloaded newer image for $image", null, "")140        )141    }142    should("fail when pulling a non-existent image").onlyIfDockerDaemonPresent {143        val exception = shouldThrow<ImagePullFailedException> {144            client.pullImage(imageThatDoesNotExist)145        }146        // Docker returns a different error message depending on whether or not the user is logged in to the source registry147        exception.message shouldBeIn setOf(148            // User is logged in149            "Error response from daemon: manifest for batect/this-image-does-not-exist:abc123 not found: manifest unknown: manifest unknown",150            // User is not logged in151            "Error response from daemon: pull access denied for batect/this-image-does-not-exist, repository does not exist or may require 'docker login': denied: requested access to the resource is denied"152        )153    }154    should("fail when pulling an image for another platform").onlyIfDockerDaemonPresent {155        val imageForOtherPlatform = when (testEnvironmentContainerOperatingSystem) {156            ContainerOperatingSystem.Linux -> "mcr.microsoft.com/windows/nanoserver:ltsc2022"157            ContainerOperatingSystem.Windows -> "gcr.io/distroless/static:063a079c1a87bad3369cb9daf05e371e925c0c91@sha256:aadea1b1f16af043a34491eec481d0132479382096ea34f608087b4bef3634be"158        }159        val exception = shouldThrow<ImagePullFailedException> {160            client.pullImage(imageForOtherPlatform)161        }162        val expectedMessages = when (testEnvironmentContainerOperatingSystem) {163            ContainerOperatingSystem.Linux -> setOf(164                "no matching manifest for linux/amd64 in the manifest list entries",165                "no matching manifest for linux/arm64/v8 in the manifest list entries",166                "image operating system \"windows\" cannot be used on this platform"167            )168            ContainerOperatingSystem.Windows -> setOf(169                "no matching manifest for windows/amd64 in the manifest list entries",170                "image operating system \"linux\" cannot be used on this platform"171            )172        }173        exception.message shouldBeIn expectedMessages174    }175    should("return null when getting a non-existent image").onlyIfDockerDaemonPresent {176        val imageReference = client.getImage(imageThatDoesNotExist)177        imageReference shouldBe null178    }179    should("fail when deleting a non-existent image").onlyIfDockerDaemonPresent {180        val exception = shouldThrow<ImageDeletionFailedException> {181            client.deleteImage(ImageReference("this-image-does-not-exist"))182        }183        exception.message shouldBe "No such image: this-image-does-not-exist"184    }185})186internal fun DockerClient.deleteImageIfPresent(name: String) {187    val image = getImage(name)188    if (image != null) {189        deleteImage(image, force = true)190    }191}...ManagePlaylistActionTest.kt
Source:ManagePlaylistActionTest.kt  
...28import fr.nihilus.music.core.media.MediaId.Builder.TYPE_TRACKS29import fr.nihilus.music.core.test.coroutines.CoroutineTestRule30import fr.nihilus.music.core.test.os.TestClock31import io.kotest.assertions.assertSoftly32import io.kotest.assertions.extracting33import io.kotest.assertions.throwables.shouldThrow34import io.kotest.inspectors.forAll35import io.kotest.inspectors.forNone36import io.kotest.matchers.collections.shouldBeEmpty37import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder38import io.kotest.matchers.collections.shouldHaveSize39import io.kotest.matchers.collections.shouldNotContain40import io.kotest.matchers.file.shouldBeAFile41import io.kotest.matchers.file.shouldContainFile42import io.kotest.matchers.file.shouldNotBeEmpty43import io.kotest.matchers.file.shouldNotExist44import io.kotest.matchers.shouldBe45import kotlinx.coroutines.test.runTest46import org.junit.Rule47import org.junit.rules.RuleChain48import org.junit.rules.TemporaryFolder49import org.junit.runner.RunWith50import java.io.File51import kotlin.test.Test52private const val TEST_TIME = 1585662510L53private const val NEW_PLAYLIST_NAME = "My favorites"54private const val BASE_ICON_URI = "content://fr.nihilus.music.test.provider/icons"55private val SAMPLE_PLAYLIST = Playlist(56    id = 1L,57    title = "Zen",58    created = 0L,59    iconUri = "content://fr.nihilus.music.test.provider/icons/zen.png".toUri()60)61@RunWith(AndroidJUnit4::class)62internal class ManagePlaylistActionTest {63    private val test = CoroutineTestRule()64    private val iconDir = TemporaryFolder()65    @get:Rule66    val rules: RuleChain = RuleChain67        .outerRule(test)68        .around(iconDir)69    private val clock = TestClock(TEST_TIME)70    private val dispatchers = AppDispatchers(test.dispatcher)71    @Test72    fun `When creating a playlist without tracks then record it to PlaylistDao`() = test {73        val dao = InMemoryPlaylistDao()74        val action = ManagePlaylistAction(dao)75        action.createPlaylist(NEW_PLAYLIST_NAME, emptyList())76        val playlists = dao.savedPlaylists77        playlists shouldHaveSize 178        assertSoftly(playlists[0]) {79            title shouldBe NEW_PLAYLIST_NAME80            created shouldBe TEST_TIME81            iconUri shouldBe "content://fr.nihilus.music.test.provider/icons/My_favorites.png".toUri()82        }83        dao.savedTracks.shouldBeEmpty()84    }85    @Test86    fun `When creating a playlist then generate and save its icon`() = test {87        val action = ManagePlaylistAction(InMemoryPlaylistDao())88        action.createPlaylist(NEW_PLAYLIST_NAME, emptyList())89        iconDir.root shouldContainFile "My_favorites.png"90        val iconFile = File(iconDir.root, "My_favorites.png")91        iconFile.shouldBeAFile()92        iconFile.shouldNotBeEmpty()93    }94    @Test95    fun `Given blank name, when creating a playlist then fail with IAE`() = test {96        val action = ManagePlaylistAction(InMemoryPlaylistDao())97        shouldThrow<IllegalArgumentException> {98            action.createPlaylist("", emptyList())99        }100        shouldThrow<IllegalArgumentException> {101            action.createPlaylist("  \t\n\r", emptyList())102        }103    }104    @Test105    fun `When creating a playlist with tracks then record them to PlaylistDao`() = test {106        val dao = InMemoryPlaylistDao()107        val action = ManagePlaylistAction(dao)108        action.createPlaylist(109            name = NEW_PLAYLIST_NAME,110            members = listOf(111                MediaId(TYPE_TRACKS, CATEGORY_ALL, 16L),112                MediaId(TYPE_TRACKS, CATEGORY_ALL, 42L)113            )114        )115        val playlists = dao.savedPlaylists116        playlists shouldHaveSize 1117        val newPlaylist = playlists[0]118        newPlaylist.title shouldBe NEW_PLAYLIST_NAME119        newPlaylist.created shouldBe TEST_TIME120        val tracks = dao.savedTracks121        tracks shouldHaveSize 2122        tracks.forAll { it.playlistId shouldBe newPlaylist.id }123        extracting(tracks) { trackId }.shouldContainExactlyInAnyOrder(16L, 42L)124    }125    @Test126    fun `When creating a playlist with non-track members them fail with IAE`() = test {127        val action = ManagePlaylistAction(InMemoryPlaylistDao())128        for (mediaId in invalidTrackIds()) {129            shouldThrow<IllegalArgumentException> {130                action.createPlaylist(131                    name = NEW_PLAYLIST_NAME,132                    members = listOf(mediaId)133                )134            }135        }136    }137    @Test138    fun `When appending members then add tracks to that playlist`() = test {139        val dao = InMemoryPlaylistDao(initialPlaylists = listOf(SAMPLE_PLAYLIST))140        val action = ManagePlaylistAction(dao)141        action.appendMembers(142            targetPlaylist = MediaId(TYPE_PLAYLISTS, SAMPLE_PLAYLIST.id.toString()),143            members = listOf(144                MediaId(TYPE_TRACKS, CATEGORY_ALL, 16L),145                MediaId(TYPE_TRACKS, CATEGORY_ALL, 42L)146            )147        )148        val tracks = dao.savedTracks149        tracks shouldHaveSize 2150        tracks.forAll { it.playlistId shouldBe SAMPLE_PLAYLIST.id }151        extracting(tracks) { trackId }.shouldContainExactlyInAnyOrder(16L, 42L)152    }153    @Test154    fun `Given invalid target media id, when appending members then fail with IAE`() = test {155        val dao = InMemoryPlaylistDao(initialPlaylists = listOf(SAMPLE_PLAYLIST))156        val action = ManagePlaylistAction(dao)157        val newMemberIds = listOf(158            MediaId(TYPE_TRACKS, CATEGORY_ALL, 16L),159            MediaId(TYPE_TRACKS, CATEGORY_ALL, 42L)160        )161        for (mediaId in invalidPlaylistIds()) {162            shouldThrow<IllegalArgumentException> {163                action.appendMembers(164                    targetPlaylist = mediaId,165                    members = newMemberIds...DeleteTracksActionTest.kt
Source:DeleteTracksActionTest.kt  
...22import fr.nihilus.music.core.media.MediaId.Builder.TYPE_PLAYLISTS23import fr.nihilus.music.core.media.MediaId.Builder.TYPE_TRACKS24import fr.nihilus.music.media.provider.DeleteTracksResult25import fr.nihilus.music.media.provider.Track26import io.kotest.assertions.extracting27import io.kotest.assertions.throwables.shouldThrow28import io.kotest.matchers.collections.shouldNotContain29import io.kotest.matchers.shouldBe30import io.kotest.matchers.types.shouldBeInstanceOf31import kotlinx.coroutines.flow.first32import kotlinx.coroutines.test.runTest33import kotlin.test.Test34private val SAMPLE_TRACKS = listOf(35    Track(36        161,37        "1741 (The Battle of Cartagena)",38        "Alestorm",39        "Sunset on the Golden Age",40        437603,41        1,42        4,43        "",44        null,45        1466283480,46        26,47        65,48        17_506_48149    ),50    Track(51        309,52        "The 2nd Law: Isolated System",53        "Muse",54        "The 2nd Law",55        300042,56        1,57        13,58        "",59        null,60        1439653800,61        18,62        40,63        12_075_96764    ),65    Track(66        481,67        "Dirty Water",68        "Foo Fighters",69        "Concrete and Gold",70        320914,71        1,72        6,73        "",74        null,75        1506374520,76        13,77        102,78        12_912_28279    ),80    Track(81        48,82        "Give It Up",83        "AC/DC",84        "Greatest Hits 30 Anniversary Edition",85        233592,86        1,87        19,88        "",89        null,90        1455310080,91        5,92        7,93        5_716_57894    ),95    Track(96        125,97        "Jailbreak",98        "AC/DC",99        "Greatest Hits 30 Anniversary Edition",100        276668,101        2,102        14,103        "",104        null,105        1455310140,106        5,107        7,108        6_750_404109    ),110    Track(111        294,112        "Knights of Cydonia",113        "Muse",114        "Black Holes and Revelations",115        366946,116        1,117        11,118        "",119        null,120        1414880700,121        18,122        38,123        11_746_572124    ),125    Track(126        219,127        "A Matter of Time",128        "Foo Fighters",129        "Wasting Light",130        276140,131        1,132        8,133        "",134        null,135        1360677660,136        13,137        26,138        11_149_678139    ),140    Track(141        75,142        "Nightmare",143        "Avenged Sevenfold",144        "Nightmare",145        374648,146        1,147        1,148        "",149        null,150        1439590380,151        4,152        6,153        10_828_662154    ),155    Track(156        464,157        "The Pretenders",158        "Foo Fighters",159        "Echoes, Silence, Patience & Grace",160        266509,161        1,162        1,163        "",164        null,165        1439653740,166        13,167        95,168        4_296_041169    ),170    Track(171        477,172        "Run",173        "Foo Fighters",174        "Concrete and Gold",175        323424,176        1,177        2,178        "",179        null,180        1506374520,181        13,182        102,183        13_012_576184    )185)186/**187 * Verify behavior of [DeleteTracksAction].188 */189internal class DeleteTracksActionTest {190    @Test191    fun `Given invalid track media ids, when deleting then fail with IAE`() = runTest {192        val dao = InMemoryTrackDao()193        val action = DeleteTracksAction(dao)194        val invalidTrackIds = listOf(195            MediaId(TYPE_TRACKS, CATEGORY_ALL),196            MediaId(TYPE_ALBUMS, "13"),197            MediaId(TYPE_ARTISTS, "78"),198            MediaId(TYPE_PLAYLISTS, "9")199        )200        for (mediaId in invalidTrackIds) {201            shouldThrow<IllegalArgumentException> {202                action.delete(listOf(mediaId))203            }204        }205    }206    @Test207    fun `When deleting tracks then remove records from dao`() = runTest {208        val dao = InMemoryTrackDao(initial = SAMPLE_TRACKS)209        val action = DeleteTracksAction(dao)210        val deleteResult = action.delete(211            mediaIds = listOf(212                MediaId(TYPE_TRACKS, CATEGORY_ALL, 161),213                MediaId(TYPE_TRACKS, CATEGORY_ALL, 48),214                MediaId(TYPE_TRACKS, CATEGORY_ALL, 75)215            )216        )217        deleteResult.shouldBeInstanceOf<DeleteTracksResult.Deleted>()218        deleteResult.count shouldBe 3219        val savedTracks = dao.tracks.first()220        savedTracks.size shouldBe 7221        extracting(savedTracks) { id }.also {222            it shouldNotContain 161223            it shouldNotContain 48224            it shouldNotContain 75225        }226    }227    @Test228    fun `Given denied permission, when deleting tracks then return RequiresPermission`() = runTest {229        val deniedDao = InMemoryTrackDao(permissionGranted = false)230        val action = DeleteTracksAction(deniedDao)231        val targetTrackIds = listOf(232            MediaId(TYPE_TRACKS, CATEGORY_ALL, 161),233            MediaId(TYPE_TRACKS, CATEGORY_ALL, 464)234        )235        val result = action.delete(targetTrackIds)...UserHashedAuthenticationPasswordSpec.kt
Source:UserHashedAuthenticationPasswordSpec.kt  
...6import stasis.client_android.lib.encryption.secrets.UserHashedAuthenticationPassword7import java.util.UUID8class UserHashedAuthenticationPasswordSpec : WordSpec({9    "A UserHashedAuthenticationPassword" should {10        "allow extracting the hashed password" {11            val originalPassword = "test-password"12            val expectedPassword = "dGVzdC1wYXNzd29yZA"13            val actualPassword = UserHashedAuthenticationPassword(14                user = UUID.randomUUID(),15                hashedPassword = originalPassword.encodeUtf8()16            )17            actualPassword.extract() shouldBe (expectedPassword)18        }19        "fail if the password is extracted more than once" {20            val originalPassword = "test-password"21            val expectedPassword = "dGVzdC1wYXNzd29yZA"22            val actualPassword = UserHashedAuthenticationPassword(23                user = UUID.randomUUID(),24                hashedPassword = originalPassword.encodeUtf8()...ExtractTest.kt
Source:ExtractTest.kt  
1package com.sksamuel.kotest2import io.kotest.assertions.extracting3import io.kotest.assertions.throwables.shouldThrowAny4import io.kotest.core.spec.style.WordSpec5import io.kotest.matchers.collections.shouldContainAll6import io.kotest.matchers.shouldBe7class ExtractTest : WordSpec() {8  init {9    data class Person(val name: String, val age: Int, val friends: List<Person>)10    val p1 = Person("John Doe", 20, emptyList())11    val p2 = Person("Samantha Rose", 19, listOf(p1))12    val persons = listOf(p1, p2)13    "extracting" should {14      "extract simple properties"{15        extracting(persons) { name }16            .shouldContainAll("John Doe", "Samantha Rose")17      }18      "extract complex properties"{19        extracting(persons) { Pair(name, age) }20            .shouldContainAll(21                Pair("John Doe", 20),22                Pair("Samantha Rose", 19)23            )24      }25      "fail if the matcher fails"{26        shouldThrowAny {27          extracting(persons) { name }28              .shouldContainAll("<Some name that is wrong>")29        }.message shouldBe """Collection should contain all of ["<Some name that is wrong>"] but was missing ["<Some name that is wrong>"]"""30      }31    }32  }33}extracting.kt
Source:extracting.kt  
1package io.kotest.assertions2/**3 * `extracting` pulls property values out of a list of objects for _typed_ bulk assertions on properties.4 *5 * The **simple example** shows how `extracting` helps with disjunct collection assertions:6 * ```7 * extracting(persons){ name }8 *   .shouldContainAll("John Doe", "Samantha Roes")9 * ```10 *11 * This is similar to using multiple [forOne] however allows for a more concise notation.12 * ```13 * forOne(persons){ it.name shouldBe "John Doe" }14 * forOne(persons){ it.name shouldBe "Samantha Rose" }15 * ```16 *17 * `extracting` also allows to define complex return types shown in this **elaborate example**:18 * ```19 * extracting(persons){ Pair(name, age) }20 *   .shouldContainAll(21 *     Pair("John Doe", 20),22 *     Pair("Samantha Roes", 19)23 *   )24 * ```25 * @param col the collection of objects from which to extract the properties26 * @param extractor the extractor that defines _which_ properties are returned27 * @author Hannes Thaller28 */29fun <K, T> extracting(col: Collection<K>, extractor: K.() -> T): List<T> {30  return col.map(extractor)31}...extracting
Using AI Code Generation
1    import io.kotest.assertions.extracting2    import io.kotest.matchers.shouldNotBe3    import io.kotest.matchers.comparables.extract4    class ExtractingTest {5        fun `extracting`() {6            data class Person(val name: String, val age: Int)7            val person = Person("John", 30)8            person.extracting(Person::name, Person::age) shouldBe listOf("John", 30)9            person.extracting(Person::name, Person::age) shouldNotBe listOf("John", 20)10            person.extracting(Person::name, Person::age) shouldBe listOf(extract { it shouldNotBe "John" }, 30)11        }12    }extracting
Using AI Code Generation
1import io.kotest.assertions.Extractor2val extractor = Extractor<Int, String> { it.toString() }3val result = extractor.extract(1)4import io.kotest.assertions.Extractor5val extractor = Extractor { it.toString() }6val result = extractor.extract(1)7import io.kotest.assertions.Extractor8val extractor = Extractor<Int, String> { it.toString() }9val result = extractor.extract(1)10import io.kotest.assertions.Extractor11val extractor = Extractor { it.toString() }12val result = extractor.extract(1)13import io.kotest.assertions.Extractor14val extractor = Extractor<Int, String> { it.toString() }15val result = extractor.extract(1)16import io.kotest.assertions.Extractor17val extractor = Extractor { it.toString() }18val result = extractor.extract(1)19import io.kotest.assertions.Extractor20val extractor = Extractor<Int, String> { it.toString() }21val result = extractor.extract(1)22import io.kotest.assertions.Extractor23val extractor = Extractor { it.toString() }24val result = extractor.extract(1)25import io.kotest.assertions.Extractor26val extractor = Extractor<Int, String> { it.toString() }27val result = extractor.extract(1)28import io.kotest.assertions.Extractor29val extractor = Extractor { it.toString() }30val result = extractor.extract(1)31import io.kotest.assertions.Extractor32val extractor = Extractor<Int, String> { it.toString() }33val result = extractor.extract(1)34importextracting
Using AI Code Generation
1result.shouldBeFailure()2result.exceptionOrNull() shouldHaveType ArithmeticException::class3result.exceptionOrNull() shouldHaveMessage "/ by zero"4result.exceptionOrNull() shouldHaveCause IllegalArgumentException::class5result.exceptionOrNull() shouldHaveCauseMessage "some message"6result.shouldBeFailure()7result.exceptionOrNull() shouldHaveType ArithmeticException::class8result.exceptionOrNull() shouldHaveMessage "/ by zero"9result.exceptionOrNull() shouldHaveCause IllegalArgumentException::class10result.exceptionOrNull() shouldHaveCauseMessage "some message"11result.shouldBeFailure()12result.exceptionOrNull() shouldHaveType ArithmeticException::class13result.exceptionOrNull() shouldHaveMessage "/ by zero"14result.exceptionOrNull() shouldHaveCause IllegalArgumentException::class15result.exceptionOrNull() shouldHaveCauseMessage "some message"16result.shouldBeFailure()17result.exceptionOrNull() shouldHaveType ArithmeticException::class18result.exceptionOrNull() shouldHaveMessage "/ by zero"19result.exceptionOrNull() shouldHaveCause IllegalArgumentException::class20result.exceptionOrNull() shouldHaveCauseMessage "some message"21result.shouldBeFailure()22result.exceptionOrNull() shouldHaveType ArithmeticException::class23result.exceptionOrNull() shouldHaveMessage "/ by zero"24result.exceptionOrNull() shouldHaveCause IllegalArgumentException::class25result.exceptionOrNull() shouldHaveCauseMessage "some message"26result.shouldBeFailure()27result.exceptionOrNull() shouldHaveType ArithmeticException::class28result.exceptionOrNull() shouldHaveMessage "/ by zero"29result.exceptionOrNull() shouldHaveCause IllegalArgumentException::class30result.exceptionOrNull() shouldHaveCauseMessage "some message"31result.shouldBeFailure()32result.exceptionOrNull() shouldHaveType ArithmeticException::classLearn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!
