Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Java → kotlin: Tests Made Simple

1,994 views

Published on

Heisenbug St. Petersburg, 04.06.2017

Published in: Software
  • Be the first to comment

Java → kotlin: Tests Made Simple

  1. 1. Java → Kotlin: Tests Made Simple — Leonid Rudenko
  2. 2. _2 Why? —
  3. 3. _3 Why? —
  4. 4. • Reference http://kotlinlang.org/docs/reference/ _5 Useful links —
  5. 5. • Reference http://kotlinlang.org/docs/reference/ • List of Kotlin resources https://kotlin.link/ _6 Useful links —
  6. 6. • Reference http://kotlinlang.org/docs/reference/ • List of Kotlin resources https://kotlin.link/ • Try Kotlin online http://try.kotl.in/ _7 Useful links —
  7. 7. • Reference http://kotlinlang.org/docs/reference/ • List of Kotlin resources https://kotlin.link/ • Try Kotlin online http://try.kotl.in/ • Slack https://kotlinlang.slack.com/ _8 Useful links —
  8. 8. • Problems Kotlin solves • Kotlin & Frameworks • Demo _9 What’s going on here —
  9. 9. • Problems Kotlin solves • Kotlin & Frameworks • Demo _10 What’s going on here —
  10. 10. // Java Map<Integer, Credentials> users = new HashMap<>(); users.put(1, new Credentials("vasya", "123456")); users.put(2, new Credentials("johny", "qwerty")); users.put(3, new Credentials("admin", "admin")); List<Integer> responseCodes = new ArrayList<>(); responseCodes.add(200); responseCodes.add(302); _11 1. Problem: Collections in Java —
  11. 11. // Java List<String> classpath = new ArrayList<>(); classpath.add(getBundleJarPath()); classpath.addAll(getPluginsPath()); _12 1. allure-framework/allure1 https://git.io/v9RQZ —
  12. 12. // Java List<String> classpath = new ArrayList<>(); classpath.add(getBundleJarPath()); classpath.addAll(getPluginsPath()); // Kotlin val classpath = listOf(getBundleJarPath(), getPluginsPath()) _13 1. allure-framework/allure1 https://git.io/v9RQZ —
  13. 13. // Java List<String> classpath = new ArrayList<>(); classpath.add(getBundleJarPath()); classpath.addAll(getPluginsPath()); // Kotlin val classpath = listOf(getBundleJarPath(), getPluginsPath()) List<String> _14 1. allure-framework/allure1 https://git.io/v9RQZ —
  14. 14. // Java List<String> classpath = new ArrayList<>(); classpath.add(getBundleJarPath()); classpath.addAll(getPluginsPath()); // Kotlin val classpath = listOf(getBundleJarPath(), getPluginsPath()) List<String> classpath.add("/usr/bin") _15 1. allure-framework/allure1 https://git.io/v9RQZ —
  15. 15. // Java Map<TestItemIssueType, List<StatisticSubType>> types = new HashMap<>() _16 1. reportportal/service-api https://git.io/v9RQy —
  16. 16. // Java Map<TestItemIssueType, List<StatisticSubType>> types = new HashMap<>() {{ }}; _17 1. reportportal/service-api https://git.io/v9RQy —
  17. 17. // Java Map<TestItemIssueType, List<StatisticSubType>> types = new HashMap<>() {{ put(AUTOMATION_BUG, Lists.newArrayList( new StatisticSubType(AUTOMATION_BUG.getLocator(), AUTOMATION_BUG.getValue(), "Automation Bug", "AB", "#f5d752"))); }}; _18 1. reportportal/service-api https://git.io/v9RQy —
  18. 18. // Java Map<TestItemIssueType, List<StatisticSubType>> types = new HashMap<>() {{ put(AUTOMATION_BUG, Lists.newArrayList( new StatisticSubType(AUTOMATION_BUG.getLocator(), AUTOMATION_BUG.getValue(), "Automation Bug", "AB", "#f5d752"))); ... put(TO_INVESTIGATE, Lists.newArrayList( new StatisticSubType(TO_INVESTIGATE.getLocator(), TO_INVESTIGATE.getValue(), "To Investigate", "TI", "#ffa500"))); }}; _19 1. reportportal/service-api https://git.io/v9RQy —
  19. 19. // Java val types = mapOf( AUTOMATION_BUG to listOf(StatisticSubType(AUTOMATION_BUG.locator, AUTOMATION_BUG.value, "Automation Bug", "AB", "#f5d752")), ... TO_INVESTIGATE to listOf(StatisticSubType(TO_INVESTIGATE.locator, TO_INVESTIGATE.value, "To Investigate", "TI", "#ffa500"))) _20 1. reportportal/service-api https://git.io/v9RQy —
  20. 20. _21 1. Collections: Traversing a map — // Java for (Map.Entry<Integer, Credentials> pair : users.entrySet()) { System.out.println(pair.getKey() + "->" + pair.getValue()); } // Kotlin users.forEach { k, v -> println("$k->$v") }
  21. 21. _22 1. allure-framework/allure1 https://git.io/v9R7E — // Java List<String> names = new ArrayList<>(); for (File file : files) { TestSuiteResult result = JAXB.unmarshal(file, TestSuiteResult.class); names.add(result.getName()); }
  22. 22. _23 1. allure-framework/allure1 https://git.io/v9R7E — // Java List<String> names = new ArrayList<>(); for (File file : files) { TestSuiteResult result = JAXB.unmarshal(file, TestSuiteResult.class); names.add(result.getName()); } // Kotlin val names = files.map { JAXB.unmarshal(it, TestSuiteResult::class.java).name }
  23. 23. • Java 7 • Java 8 (Stream API) • Groovy _24 1. Collections: Java & Groovy —
  24. 24. _25 2. Problem: framework can’t do what you need it to do — // Java public static void waitForElement(HtmlElement element, long timeout) { ... } waitForElement(link, 10); link.click();
  25. 25. _26
  26. 26. _27 2. Problem: framework can’t do what you need it to do —
  27. 27. _28 2. Solution: Extension functions — // Kotlin fun <T : HtmlElement> T.waitForIt(timeout: Long = 5): T { ... return this } link.waitForIt().click() loginForm.waitForIt(10).guestButton.waitForIt().click()
  28. 28. _29 2. Solution: Extension functions — // Kotlin fun <T : HtmlElement> T.waitForIt(timeout: Long = 5): T { ... return this } link.waitForIt().click() loginForm.waitForIt(10).guestButton.waitForIt().click()
  29. 29. _30 2. Solution: Extension functions — // Kotlin fun <T : HtmlElement> T.waitForIt(timeout: Long = 5): T { ... return this } link.waitForIt().click() loginForm.waitForIt(10).guestButton.waitForIt().click()
  30. 30. _31 2. Solution: Extension functions — // Kotlin fun <T : HtmlElement> T.waitForIt(timeout: Long = 5): T { ... return this } link.waitForIt().click() loginForm.waitForIt(10).guestButton.waitForIt().click()
  31. 31. _32 2. Solution: Extension functions —
  32. 32. • Java (Lombok https://projectlombok.org/) • Groovy (Extension Modules) _33 2. Extension functions: Java & Groovy —
  33. 33. // Java public class Credentials { private final String username; private final String password; } _34 3. Problem: small classes are not small —
  34. 34. // Java public class Credentials { private final String username; private final String password; public Credentials(String username, String password) { this.username = username; this.password = password; } } _35 3. Problem: small classes are not small —
  35. 35. // Java public class Credentials { private final String username; private final String password; public Credentials(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } public String getPassword() { return password; } } _36 3. Problem: small classes are not small —
  36. 36. // Java public class Credentials { private final String username; private final String password; public Credentials(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } public String getPassword() { return password; } @Override public String toString() { return username + '/' + password; } } _37 3. Problem: small classes are not small —
  37. 37. // Java public class Credentials { private final String username; private final String password; public Credentials(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } public String getPassword() { return password; } @Override public String toString() { return username + '/' + password; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Credentials that = (Credentials) o; if (username != null ? !username.equals(that.username) : that.username != null) return false; return password != null ? password.equals(that.password) : that.password == null; } @Override public int hashCode() { int result = username != null ? username.hashCode() : 0; return 31 * result + (password != null ? password.hashCode() : 0); } } _38 3. Problem: small classes are not small —
  38. 38. // Java public class Credentials { private final String username; private final String password; public Credentials(String username, String password) { this.username = username; this.password = password; } public String getUsername() { return username; } public String getPassword() { return password; } @Override public String toString() { return username + '/' + password; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Credentials that = (Credentials) o; if (username != null ? !username.equals(that.username) : that.username != null) return false; return password != null ? password.equals(that.password) : that.password == null; } @Override public int hashCode() { int result = username != null ? username.hashCode() : 0; return 31 * result + (password != null ? password.hashCode() : 0); } } _39 3. Problem: 27 lines —
  39. 39. _40 3. griddynamics/jagger https://git.io/v9035 —
  40. 40. _41 3. Solution: Kotlin data classes — // Kotlin data class Credentials(val username: String, val password: String) val creds = Credentials("a", "b") println(creds.username) // a creds.username = "you can't do that" println(creds) // Credentials(username=a, password=b) println(creds == Credentials("a", "b")) // true
  41. 41. • Java (Lombok https://projectlombok.org/) • Groovy (@groovy.transform.Canonical) _42 3. Data classes: Java & Groovy —
  42. 42. // Java driver.findElement("button").click(); _43 4. Problem: steps in Allure report —
  43. 43. // Java @Step("Click the button") public void clickButton() { driver.findElement("button").click(); } clickButton(); _44 4. Problem: steps in Allure report —
  44. 44. // Java 8 @Step("{0}") public void step(String title, Runnable code) { code.run(); } step("Click the button", () -> { driver.findElement("button").click(); }); _45 4. Solution: steps in Allure report —
  45. 45. // Kotlin @Step("{0}") fun step(title: String, code: () -> Any) = code() step("Click the button") { driver.findElement("button").click() } _46 4. Solution: steps in Allure report —
  46. 46. // Java 8 step("Click the button", () -> { // your code here }); // Kotlin step("Click the button") { // your code here } _47 4. Solution: just compare —
  47. 47. • Java 7 • Java 8 • Groovy _48 4. Steps in Allure report: Java & Groovy —
  48. 48. // Java URLDecoder.decode(param, "UTF-8"); _49 5. Problem: checked exceptions —
  49. 49. // Java: String to md5 try { ... } catch (NoSuchAlgorithmException ex) { throw new RuntimeException( "Unable apply MD5 algorithm for password hashing: ", ex); } catch (UnsupportedEncodingException unsEx) { throw new RuntimeException( "Unable apply UTF-8 encoding for password string: ", unsEx); } _50 5. reportportal/service-api https://git.io/v9RF5 —
  50. 50. // Java try { screenshotUrl = new URL(screenshotUrl).toExternalForm(); } catch (MalformedURLException ignore) { } _51 5. codeborne/selenide https://git.io/v9Rbe —
  51. 51. // Java try { screenshotUrl = new URL(screenshotUrl).toExternalForm(); } catch (MalformedURLException ignore) { } // Kotlin screenshotUrl = URL(screenshotUrl).toExternalForm(); _52 5. Solution: no checked exceptions —
  52. 52. • Java • Groovy _53 5. Checked exceptions: Java & Groovy —
  53. 53. // Java Object errors = executeJavaScript("return window._selenide_jsErrors"); if (errors instanceof List) { return errorsFromList((List<Object>) errors); } _54 6. Problem: unchecked cast https://git.io/v9Ek1 —
  54. 54. // Java Object errors = executeJavaScript("return window._selenide_jsErrors"); if (errors instanceof List) { return errorsFromList((List<Object>) errors); } _55 6. Problem: unchecked cast https://git.io/v9Ek1 —
  55. 55. // Java Object errors = executeJavaScript("return window._selenide_jsErrors"); if (errors instanceof List) { return errorsFromList(((List<?>) errors) .stream() .filter(Object.class::isInstance) .map(Object.class::cast) .collect(toList()) ); } _56 6. Problem: unchecked cast https://git.io/v9Ek1 —
  56. 56. // Kotlin val errors = executeJavaScript("return window._selenide_jsErrors"); if (errors is List<*>) { return errorsFromList(errors.filterIsInstance<Object>()) } _57 6. Solution: Kotlin smart cast —
  57. 57. // Kotlin val errors = executeJavaScript("return window._selenide_jsErrors"); if (errors is List<*>) { return errorsFromList(errors.filterIsInstance<Object>()) } _58 6. Solution: Kotlin smart cast —
  58. 58. • Java • Groovy _59 6. Unchecked cast: Java & Groovy —
  59. 59. _60 7. Problem: — Java is verbose
  60. 60. // Java actualText.equals(expectedText) // Kotlin actualText == expectedText _61 7. Solution: Kotlin syntactic sugar —
  61. 61. // Java String s = "date: " + date + " and author: " + USER.getName(); String s = format("date: %s and author: %s", date, USER.getName()); // Kotlin val s = "date: $date and author: ${USER.name}" _62 7. Solution: String templates —
  62. 62. // Java for (int i = 0; i <= 10; i += 2) // Kotlin for (i in 0..10 step 2) _63 7. Solution: ranges —
  63. 63. // Java !list.isEmpty() // Kotlin list.isNotEmpty() _64 7. Solution: Kotlin rich standard library —
  64. 64. • Java • Groovy _65 7. Verbosity: Java & Groovy —
  65. 65. 8. Problem: —
  66. 66. var username: String username = null // compilation error _67 8. Solution: Null safety —
  67. 67. var username: String username = null // compilation error var username: String? username = null // ok _68 8. Null safety —
  68. 68. var username: String // non-nullable String username = null var username: String? // nullable String username = null _69 8. Null safety —
  69. 69. var username: String? = "Vasya" var count = username.length // compilation error _70 8. Null safety: safe call —
  70. 70. var username: String? = "Vasya" var count = username?.length // ok, count is 5 username = null var count = username?.length // ok, count is null _71 8. Null safety: safe call —
  71. 71. // Java username != null ? username : "Guest" _72 8. Null safety: Elvis operator —
  72. 72. // Java username != null ? username : "Guest" // Kotlin username ?: "Guest" var images = findAllImagesOnPage() ?: throw AssertionError("No images!") _73 8. Null safety: Elvis operator — ?:
  73. 73. • Null safety: Java (Optional<T> in Java8), Groovy • Safe call: Java, Groovy • Elvis operator: Java, Groovy _74 8. Null safety: Java & Groovy —
  74. 74. _75 1 2 3 4 5 6 7 8 Java 7 − − − − − − − − Java 8 ± − − + − − − − Groovy + ± + + + − + − Kotlin + + + + + + + + Java vs Groovy vs Kotlin —
  75. 75. • Not statically typed: runtime bugs • Not statically typed: performance • Not statically typed: IDE support • No null safety _76 Why not Groovy? —
  76. 76. • Problems Kotlin solves • Kotlin & Frameworks • Demo _77 What’s going on here —
  77. 77. Kotlin ~ Java — // Kotlin var counter: Int = 0 // Java private int counter = 0; public final int getCounter() { return counter; } public final void setCounter(int newCounter) { counter = newCounter; } _78
  78. 78. 1. JUnit 4 — @Before fun `start browser`() { ... } @Test fun `test name`() { ... } @Ignore("ISSUE-9000") @Test fun `ignored test`() { ... } @After fun `quit browser`() { ... } _79
  79. 79. _80
  80. 80. 1. JUnit 4: @Rule — _81 // Kotlin @Rule val tempFolder = TemporaryFolder()
  81. 81. 1. JUnit 4: @Rule — _82 // Kotlin @Rule val tempFolder = TemporaryFolder() org.junit.internal.runners.rules.ValidationError: The @Rule 'tempFolder' must be public.
  82. 82. 1. JUnit 4: @Rule — _83 // Kotlin @Rule val tempFolder = TemporaryFolder() // Java @Rule private final TemporaryFolder tempFolder = new TemporaryFolder(); public final TemporaryFolder getTempFolder() { return tempFolder; }
  83. 83. 1. JUnit 4: @Rule solution #1 — _84 // Kotlin @JvmField @Rule val tempFolder = TemporaryFolder() // Java @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
  84. 84. 1. JUnit 4: @Rule solution #2 — _85 // Kotlin @get:Rule val tempFolder = TemporaryFolder() // Java private final TemporaryFolder tempFolder = new TemporaryFolder(); @Rule public final TemporaryFolder getTempFolder() { return tempFolder; }
  85. 85. 1. JUnit 4: @Parameters — _86 // Kotlin @RunWith(Parameterized::class) class ParameterizedTest { @Parameters(name = "{0}") fun data(): Collection<Array<String>> = asList( arrayOf("firefox User-Agent"), arrayOf("chrome User-Agent") ) @Parameter lateinit var userAgent: String }
  86. 86. 1. JUnit 4: @Parameters — _87 // Kotlin @RunWith(Parameterized::class) class ParameterizedTest { @Parameters(name = "{0}") fun data(): Collection<Array<String>> = asList( arrayOf("firefox User-Agent"), arrayOf("chrome User-Agent") ) @Parameter lateinit var userAgent: String } java.lang.Exception: No public static parameters method on class com.jetbrains.ParameterizedTest
  87. 87. Companion object — // Kotlin class MyClass { companion object { fun looksLikeStatic() { ... } } } MyClass.looksLikeStatic() _88
  88. 88. 1. JUnit 4: @Parameters — _89 // Kotlin @RunWith(Parameterized::class) class ParameterizedTest { companion object { @Parameters(name = "{0}") fun data(): Collection<Array<String>> = asList( arrayOf("firefox User-Agent"), arrayOf("chrome User-Agent") ) } @Parameter lateinit var userAgent: String }
  89. 89. 1. JUnit 4: @Parameters — _90 // Kotlin @RunWith(Parameterized::class) class ParameterizedTest { companion object { @Parameters(name = "{0}") fun data(): Collection<Array<String>> = asList( arrayOf("firefox User-Agent"), arrayOf("chrome User-Agent") ) } @Parameter lateinit var userAgent: String } java.lang.Exception: No public static parameters method on class com.jetbrains.ParameterizedTest
  90. 90. 1. JUnit 4: @Parameters solution — _91 // Kotlin @RunWith(Parameterized::class) class ParameterizedTest { companion object { @Parameters(name = "{0}") @JvmStatic fun data(): Collection<Array<String>> = asList( arrayOf("firefox User-Agent"), arrayOf("chrome User-Agent") ) } @Parameter lateinit var userAgent: String }
  91. 91. 1. JUnit 4: @Parameters solution — _92 // Java @RunWith(Parameterized.class) class ParameterizedTest { public final static class Companion { public Collection<String[]> data() { return asList(...); } } public final static Companion Companion = new Companion(); @Parameters(name = "{0}") public final static Collection<String[]> data() { return Companion.data(); } }
  92. 92. 1. JUnit 4: @Parameters solution — _93 // Java @RunWith(Parameterized.class) class ParameterizedTest { public final static class Companion { public Collection<String[]> data() { return asList(...); } } public final static Companion Companion = new Companion(); @Parameters(name = "{0}") public final static Collection<String[]> data() { return Companion.data(); } }
  93. 93. 1. JUnit 4: @Parameters solution — _94 // Java @RunWith(Parameterized.class) class ParameterizedTest { public final static class Companion { public Collection<String[]> data() { return asList(...); } } public final static Companion Companion = new Companion(); @Parameters(name = "{0}") public final static Collection<String[]> data() { return Companion.data(); } }
  94. 94. 1. JUnit 4: @Parameters solution — _95 // Java @RunWith(Parameterized.class) class ParameterizedTest { public final static class Companion { public Collection<String[]> data() { return asList(...); } } public final static Companion Companion = new Companion(); @Parameters(name = "{0}") public final static Collection<String[]> data() { return Companion.data(); } }
  95. 95. 2. HtmlElements 1.*: element — _96 @FindBy(css = "form[name='LoginForm']") class LoginForm : HtmlElement() { @FindBy(css = "#username") lateinit var usernameInput: HtmlElement @FindBy(css = "#password") lateinit var passwordInput: HtmlElement @FindBy(xpath = ".//button") lateinit var loginButton: HtmlElement fun login(username: String, password: String) { usernameInput.sendKeys(username) passwordInput.sendKeys(password) loginButton.click() } }
  96. 96. 2. HtmlElements 1.*: page object — _97 class Page(val driver: WebDriver) { init { PageFactory.initElements( HtmlElementDecorator( HtmlElementLocatorFactory(driver)), this) } lateinit var loginForm: LoginForm @FindBy(css = ".dashboard-buttons_add") lateinit var addWidgetButton: HtmlElement }
  97. 97. _98
  98. 98. 2. HtmlElements 1.*: collection of elements — _99 // Kotlin @FindBy(css = "a") lateinit var links: List<HtmlElement> ... links.forEach { ... }
  99. 99. 2. HtmlElements 1.*: collection of elements — _100 // Kotlin @FindBy(css = "a") lateinit var links: List<HtmlElement> ... links.forEach { ... } kotlin.UninitializedPropertyAccessException
  100. 100. 2. HtmlElements 1.*: collection of elements — _101 // Kotlin @FindBy(css = "a") lateinit var links: List<HtmlElement> java.util.List<? extends ru.yandex.qatools.htmlelements.element.HtmlElement>
  101. 101. 2. HtmlElements 1.*: collection of elements — _102 // Kotlin @FindBy(css = "a") lateinit var links: List<@JvmSuppressWildcards HtmlElement> java.util.List<ru.yandex.qatools.htmlelements.element.HtmlElement> • https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#variant-generics
  102. 102. 3. Allure — _103 // Kotlin @Title("This is title") @Description("This is description") @Test fun test() { ... } @Step("Change Home URL to {0}") fun changeHomeURL(homeURL: String) { ... } @Attachment(value = "{0}", type = "text/plain") fun attachText(name: String = "text", text: String?) = text
  103. 103. _104
  104. 104. 3. Allure: org.aspectj:aspectjweaver:1.8.10 — _105 // Kotlin links.forEach { println(it.text) }
  105. 105. 3. Allure: org.aspectj:aspectjweaver:1.8.10 — _106 // Kotlin links.forEach { println(it.text) } java.lang.ClassFormatError: Invalid index 6 in LocalVariableTable
  106. 106. 3. Allure: org.aspectj:aspectjweaver:1.8.10 — _107 // Kotlin links.forEach { println(it.text) } java.lang.ClassFormatError: Invalid index 6 in LocalVariableTable • https://bugs.eclipse.org/bugs/show_bug.cgi?id=500796
  107. 107. 4. Selenide: $ is reserved — _108 // Kotlin import com.codeborne.selenide.Selenide.$ Error: Kotlin: Qualified name must be a '.'-separated identifier list
  108. 108. 4. Selenide: $ is reserved — _109 // Kotlin import com.codeborne.selenide.Selenide.`$` import com.codeborne.selenide.Selenide.`$$` ... `$`("a") `$$`("a")
  109. 109. _110
  110. 110. 5. Selenium WebDriver — _111 // Kotlin val driver = RemoteWebDriver( URL("http://grid.company.com:4444/wd/hub"), DesiredCapabilities.chrome()) driver.manage().window().size = Dimension(800, 600) driver.get("https://heisenbug-piter.ru") val link = driver.findElement(By.cssSelector("a.navbar- brand")) val text = link.text val clazz = link.getAttribute("class") Actions(driver).moveToElement(link).perform() (driver as TakesScreenshot).getScreenshotAs(OutputType.BYTES) driver.quit()
  111. 111. _112
  112. 112. 6. REST Assured — _113 // Kotlin fun createIssue(issue: Issue): Issue = given() .baseUri("http://host:8080") .header("Authorization", "*token*") .contentType("application/json") .accept("application/json") .queryParams(mapOf("fields" to "id,project(id,shortName),numberInProject")) .with().body(issue) .post("/api/issues") .`as`(Issue::class.java)
  113. 113. • Problems Kotlin solves • Kotlin & Frameworks • Demo _114 What’s going on here —
  114. 114. _115 Example web tests project in Kotlin — gradle-docker-plugin
  115. 115. _116 Example web tests project in Kotlin — gradle-docker-plugin JUnit, Html Elements
  116. 116. _117 Example web tests project in Kotlin — gradle-docker-plugin JUnit, Html Elements
  117. 117. _118 • Kotlin + { Gradle, JUnit, Selenium, Html Elements, Allure } = OK https://github.com/leonsabr/web-tests-in-kotlin-demo —
  118. 118. _119 • Kotlin + { Gradle, JUnit, Selenium, Html Elements, Allure } • Java interoperability https://github.com/leonsabr/web-tests-in-kotlin-demo —
  119. 119. _120 https://github.com/leonsabr/web-tests-in-kotlin-demo —
  120. 120. _121 java kotlin ∆ main 675 434 35,7% test 89 84 5,6% total 764 518 32,2% • Kotlin + { Gradle, JUnit, Selenium, Html Elements, Allure } • Java interoperability • Conciseness (lines of code) https://github.com/leonsabr/web-tests-in-kotlin-demo —
  121. 121. Thank you for your attention — jetbrains.com leonid.rudenko@jetbrains.com @leonsabr

×