59
@codecopkoer · @g_o_rge JUnit 5 © Peter Koer & Görge Albrecht 2017 JUnit Writing tests with Java 8 and JUnit

JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Page 1: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

@codecopko�er · @g_o_rge JUnit 5 © Peter Ko�er & Görge Albrecht2017

JUnit

Writing tests with Java 8 and JUnit

Page 2: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

Peter Ko�erProfessional Software Developer for 15+ years

“fanatic about code quality”

I help development teams!

@codecopko�er · code-cop.org

Page 3: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

1

2

Görge AlbrechtSoftware Developer since 1989

Code Mentor - taking care of code

Need help writing simpler code? Contact me!

@g_o_rge · code-mentor.com

Page 4: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

3

Page 5: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

5 . 1

The Beginnings

Page 6: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

4

SUnit ⇒ JUnitSUnit written by Kent Beck around 1992

JUnit created by Kent Beck and Erich Gamma around 1997

JUnit 4 - from framework style to DSL Style

added dependency to

added Rules to setting the context of a test

JUnit 4.4 Hamcrest

JUnit 4.7

Page 7: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

5 . 2

JUnit 510 Years since release of Junit 4

JUnit has always been a side project

M3 released on 2016-11-30

Crowdfunding Campaign on Indiegogo

JUnit team plans to release JUnit 5.0 GA in Q3 2017

Page 8: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

5 . 3

VisionDecouple test execution and reporting from test de�nition and provisioning

Rethinking JUnit’s extensibility story

Making use of Java 8 features

Page 9: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

6

Components

Page 10: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

7 . 1

JUnit 5 is …JUnit Platform (Engine API, Launchers)

JUnit Jupiter API (Test API)

JUnit Jupiter Engine (the test engine)

JUnit Vintage Engine (running JUnit 4)

>= 3 different dependencies.

Page 11: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

7 . 2

build.gradlebuildscript { dependencies { classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.0-M3' }}

apply plugin: 'org.junit.platform.gradle.plugin'

junitPlatform { platformVersion = "1.0.0-M3" filters { engines { include 'junit-jupiter' }}}

dependencies { testCompile "org.junit.jupiter:junit-jupiter-api:5.0.0-M3" testRuntime "org.junit.jupiter:junit-jupiter-engine:5.0.0-M3" }

Page 12: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

7 . 38 . 1

Features of JUnit 5

Page 13: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

[email protected]

@BeforeEach / @AfterEach

@BeforeAll / @AfterAll

@Disabled

Page 14: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 2

no public anymorepackage com.codementor.junit5;

import org.junit.jupiter.api.Test;

class NoPublicTest {

@Test void name() { } }

Page 15: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 3

Display Names@DisplayName("Custom names can be set on class …") class DisplayNamesTest {

@Test @DisplayName("… and method level") void name() { } }

Page 16: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 4

Dynamic Test Creation @TestFactory Stream<DynamicTest> dynamicTestCreation() { return IntStream.iterate(2, n -> n * 2).limit(4).mapToObj( n -> dynamicTest("test " + n, () -> assertTrue(n % 2 == 0)) ); }

IntelliJ support since version 2016.3

Page 17: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 5

Nested Test@DisplayName("A new int…") class NestedClassesTest {

private int count = MIN_VALUE;

@BeforeEach void setCountToZero() { count = 0; }

@Test @DisplayName("… is almost nothing…") void countIsZero() { assertEquals(0, count); }

@Nested @DisplayName("… but after a step down…")

Page 18: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 6

Nested Test Output

Page 19: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 7

Assertion Message comes last,�nally!

@Test void messageComesLast() { org.junit.Assert.assertEquals( "JUnit <=4 message comes first", "aString", "aString");

org.junit.jupiter.api.Assertions.assertEquals( "aString", "aString", "JUnit 5 message comes last"); }

Page 20: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 8

Lambdas FTW @Test void assertWithBooleanClassic() { assertTrue(true); }

@Test void assertWithBooleanLambda() { assertTrue(() -> true); }

@Test void assertWithBooleanMethodReference() { assertTrue(TRUE::booleanValue); }

Page 21: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 98 . 10

Asserting Exceptions @Test void exception() { assertThrows(RuntimeException.class, this::illegal); }

private void illegal() { throw new IllegalArgumentException("illegal"); }

Page 22: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

Asserting Exception Message @Test void exceptionMessageAsssertion() { final RuntimeException exception = assertThrows(RuntimeException.class, this::illegal); assertEquals("illegal", exception.getMessage()); }

private void illegal() { throw new IllegalArgumentException("illegal"); }

Page 23: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 118 . 12

Lazy message evaluation @Test void assertWithLambdaMessage() { assertTrue( true, () -> "JUnit 5" + " message are evaluated " + "lazily"); }

Page 24: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

Assert all properties at once @Test void assertAllProperties() { Address address = new Address("New City", "Some Street", "No");

assertAll( () -> assertEquals("New City", address.city), () -> assertEquals("Irgendeinestraße", address.street), () -> assertEquals("Nr", address.number) ); }

Page 25: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

8 . 13

Extensionsno Runner and @Rule / @ClassRule anymore

replaced by Extensions

registered via @ExtendWith on class, method or annotation

multiple extensions per element possible

registered extensions are inherited

Page 26: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

9 . 1

Extension TypesConditional Test Execution

ContainerExecutionCondition

TestExecutionCondition

Test Instance Post-processing

TestInstancePostProcessor(MockitoExtension, SpringExtension)

Parameter Resolution

ParameterResolver

Page 27: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

9 . 2

Test Lifecycle CallbacksBeforeAllCallback

BeforeEachCallback

BeforeTestExecutionCallback

AfterTestExecutionCallback

AfterEachCallback

AfterAllCallback

TestExecutionExceptionHandler

Page 28: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

9 . 3

Dependency InjectionJUnit 4.x: DI via Runner setting method parameters

JUnit 4.7: DI via @Rule settings �elds in test class

JUnit 5: ParameterResolver FTW

multiple ParameterResolver per test methodpossible

each ParameterResolver can handle differentparameter types

Page 29: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

9 . 49 . 5

DI exampleclass MyTest {

@ExtendWith(MockitoExtension.class) @ExtendWith(VertxExtension.class) @Test void test(@Mock MyService service, TestContext context) {} }

Page 30: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

10

Page 31: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

Try yourself

www.�ickr.com/photos/ninahiironniemi/497993647

Page 32: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 1

AssignmentFind a pair

Get the code (junit5-koans.zip)

Run JUnit. (Maven: mvnw test; Gradle: gradlew test)

Go through the test code

Assertions are missing/incomplete

Complete assertions, make tests pass

Page 33: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 2

Coding

Page 34: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 311 . 4

SessionsHints and solutions

Page 35: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 5

Session 1Your �rst tests

import static org.junit.jupiter.api.Assertions.assertEquals;

assertEquals();

Page 36: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

Session 2aBasic assertions

import static org.junit.jupiter.api.Assertions.*;

assertTrue(); assertFalse(); assertNull(); assertNotNull();assertArrayEquals(); assertEquals(., ., 0.01);

Page 37: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 6

Session 2bAssertions new in JUnit 5

import static org.junit.jupiter.api.Assertions.*;

assertNotEquals(); assertIterableEquals();

assertAll( () -> assertEquals(), () -> assertEquals() );

assertTimeout();

Page 38: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 7

Session 2cAssertions with Hamcrest

import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsArrayContaining.hasItemInArray; import static org.hamcrest.collection.IsArrayWithSize.arrayWithSize; import static org.hamcrest.core.IsNot.not;

assertThat(counter.uniqueWords(), hasItemInArray("bar")); assertThat(counter.uniqueWords(), not(hasItemInArray("foo")));

assertThat(counter.uniqueWords(), arrayWithSize(3));

Page 39: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 8

Session 3Fixtures

import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test;

@BeforeEach void createFreshTestFileForEachTest() throws IOException { }

@AfterEach void deleteTestFile() { }

Page 40: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 9

Session 4Exceptions and ignoring tests

import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.function.Executable;

assertThrows(Exception.class, Executable); Exception exception = assertThrows(Exception.class, Executable);

@Test @Disabled("message") void shouldCountUniqueWordsCaseInsensitive() { }

Page 41: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 10

Session 5Parameterised/table driven tests

import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory;

@TestFactory List<DynamicTest> createTests() { return TEST_CASES.stream(). map(testCase -> DynamicTest.dynamicTest( testCase.name(), testCase::shouldReturnRatioOfGivenWord) ). collect(Collectors.toList()); }

Page 42: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 11

Session 6Reuse �xtures in Extensions

import org.junit.jupiter.api.extension.ExtendWith; import org.codecop.TempFile.Temp;

@ExtendWith(TempFile.class) class SomeTest {

@Test void shouldDoSomething(@Temp File file) { // use file }

Page 43: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 12

import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ParameterResolver;

class TempFile implements BeforeEachCallback, ParameterResolver, AfterEachCallback {

public void beforeEach(TestExtensionContext context) { Object testInstance = context.getTestInstance(); createTempFileFor(testInstance); }

public Object resolve(...) { return getTempFile(); }

public void afterEach(...) {

Page 44: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 13

Coding Retrospective

Page 45: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

11 . 13

Page 47: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

RemarksJUnit 5 is a complete rewrite

neither compile nor binary compatibility

bridges provided down- and upwards for smooth migration

⇒ no need to update all existing tests

Page 48: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

13 . 2

Executionrun JUnit 5 based tests with JUnit 4 ?

JUnit 4 Runner: JUnitPlatform

run JUnit 4 based tests with JUnit 5 ?

JUnit Platform engine: junit-vintage-engine

Page 49: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

13 . 3

PackagesJUnit < 4 junit

JUnit 4.x org.junit

JUnit 5 org.junit.jupiter.api

Page 50: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

13 . 4

Assertions & AssumptionsJUnit 5 JUnit 4.x

o.j.j.a.Assertions o.j.Assert

o.j.j.a.Assumptions o.j.Assume

Page 51: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

13 . 5

AnnotationsJUnit 5 JUnit 4.x

@Test (new package) @Test

@TestFactory -

@Disabled @Ignore

@DisplayName -

@Tag @Category

Page 52: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

13 . 6

Lifecycle AnnotationsJUnit 5 JUnit 4.x

@BeforeEach @Before

@AfterEach @After

@BeforeAll @BeforeClass

@AfterAll @AfterClass

@ExtendWith ~ @RunWith, @Rule,@ClassRule

Page 53: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

13 . 713 . 8

Changing Code80% search/replace package and class names

only partial support for Rules

fails with parameterized / DynamicTest

Page 54: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

14 . 1

Switch to JUnit 5 ?

Page 55: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

well … no big improvements regarding Assertions API

⇒ use AssertJ or Hamcrest

IDEs not there yet

IntellIJ: steady progress released

use maven/gradle in the meantime

eclipse: beta branch

Page 56: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

14 . 2

but … better testing for exception

Dynamic tests are more powerfull

solves "more Runners problem"

new extension points will bring big opportunities

launcher and engine API

enables IDEs execute tests independent of the actualtesting framework

Page 57: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

14 . 3

Start today!analyze your code base and identify open ends

convert your Runner/Rule to Extension

ask your Runner/Rule provider to do so

test your favorite IDE and �le bugs

give feedback to JUnit 5 team

Page 58: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

14 . 415

What did you learn today?

Page 59: JUn i t - Code Mentorcode-mentor.com/slides/2017-02-28_TopConf_2017_Workshop_JUnit… · 1 2 Gö rg e A l b rec h t Software Developer since 1989 Code Mentor - taking care of code

16

Thanks for participating @codecopko�er · code-cop.org

@g_o_rge · code-mentor.com