Testing
Spring-based
apps
Aleksandr Barmin
CONFIDENTIAL | © 2020 EPAM Systems, Inc.
2020 EPAM Systems, Inc.
• Lead Software Engineer
• EPAM Lab Mentor
ALEKSANDR BARMIN
• Email: Aleksandr_Barmin@epam.com
• Twitter: @AlexBarmin
CONTACTS
2
2020 EPAM Systems, Inc.
Agenda
1
6
2
3
4
5
W H Y T E S T I N G I S S O I M P O R T A N T
C O N F I G U R I N G C O N T E X T F O R T E S T S
U S I N G R E A L D E P E N D E N C I E S
E X A M P L E S
3
O V E R V I E W O F T E S T I N G
T E S T L A Y E R S
2020 EPAM Systems, Inc.
Overview of testing
• A test case is a set of test inputs, execution
conditions, and expected results developed for
a particular objective, such as to exercise a
particular program path or to verify
compliance with a specific requirement.
• https://en.wikipedia.org/wiki/Test_case
4
2020 EPAM Systems, Inc.
Test Suite
Overview of testing
5
Test
Test
Test case
System Under
Test (SUT)
Verifies behavior of
2020 EPAM Systems, Inc.
Overview of testing
6
Test runner
Test class
Executes
Test method
Test method
Test method Teardown
Verify
Execute
Setup
Fixture
System Under
Test (SUT)
Configures
Restores
Interact
2020 EPAM Systems, Inc.
Overview of testing
7
Order Controller Order Service
Order Data Access
Object
Orders
Database
How to test it in isolation?
2020 EPAM Systems, Inc.
Overview of testing
8
Slow, complex
test
System Under
Test (SUT)
Dependency
Tests
Fast, simple
test
System Under
Test (SUT)
Test Double
Tests
Replaced with
2020 EPAM Systems, Inc.
Why testing is so important – Test Pyramid
9
End-to-
end
Component
Integration
UnitTest the business logic
Verify that a service
communicates with its
dependencies
Acceptance tests for a
service
Acceptance tests for an
application
Slow, brittle, costly
Fast, reliable, cheap
2020 EPAM Systems, Inc.
Why testing is so important – the Ice Cream Cone
10
End-to-end
Component
Integration
UnitTest the business logic
Verify that a service
communicates with its
dependencies
Acceptance tests for a
service
Acceptance tests for an
application
Slow, brittle, costly
Fast, reliable, cheap
2020 EPAM Systems, Inc.
Deployment pipeline
Why testing is so important - The deployment pipeline
11
Pre-commit
tests
Commit test
stage
Integration
tests stage
Component
tests stage
Deploy stage
Production
environment
Not
production
ready
Production
ready
Fast
feedback
Slow
feedback
2020 EPAM Systems, Inc.
Talk is cheap. Show me the code
- Linus Torvalds
12
2020 EPAM Systems, Inc.
The Blog Application
13
Post Controller Post Service Post Repository Post Database
2020 EPAM Systems, Inc.
Testing The Blog Application
14
Post Controller Post Service Post Repository Post Database
PostServiceSpringTest
2020 EPAM Systems, Inc.
@ContextConfiguration
15
public @interface ContextConfiguration {
}
2020 EPAM Systems, Inc.
@ContextConfiguration
16
public @interface ContextConfiguration {
// where to find bean definitions
String[] value() default {};
String[] locations() default {};
Class<?>[] classes() default {};
}
2020 EPAM Systems, Inc.
@ContextConfiguration
17
public @interface ContextConfiguration {
// where to find bean definitions
String[] value() default {};
String[] locations() default {};
Class<?>[] classes() default {};
// how to initialize the Application Context
Class<? extends ApplicationContextInitializer<?>>[] initializers() default {};
}
2020 EPAM Systems, Inc.
@ContextConfiguration
18
public @interface ContextConfiguration {
// where to find bean definitions
String[] value() default {};
String[] locations() default {};
Class<?>[] classes() default {};
// how to initialize the Application Context
Class<? extends ApplicationContextInitializer<?>>[] initializers() default {};
// should context from parent classes be loaded
boolean inheritLocations() default true;
boolean inheritInitializers() default true;
}
2020 EPAM Systems, Inc.
@ContextConfiguration
19
public @interface ContextConfiguration {
// where to find bean definitions
String[] value() default {};
String[] locations() default {};
Class<?>[] classes() default {};
// how to initialize the Application Context
Class<? extends ApplicationContextInitializer<?>>[] initializers() default {};
// should context from parent classes be loaded
boolean inheritLocations() default true;
boolean inheritInitializers() default true;
// what will read bean definitions
Class<? extends ContextLoader> loader() default ContextLoader.class;
}
2020 EPAM Systems, Inc.
@ContextConfiguration
20
public @interface ContextConfiguration {
// where to find bean definitions
String[] value() default {};
String[] locations() default {};
Class<?>[] classes() default {};
// how to initialize the Application Context
Class<? extends ApplicationContextInitializer<?>>[] initializers() default {};
// should context from parent classes be loaded
boolean inheritLocations() default true;
boolean inheritInitializers() default true;
// what will read bean definitions
Class<? extends ContextLoader> loader() default ContextLoader.class;
// name of the context hierarchy level
String name() default "";
}
2020 EPAM Systems, Inc.
@ContextConfiguration and @SpringJUnitConfig
21
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
PostService.class,
CommentValidator.class,
PostSanitizer.class
})
public class PostServiceSpringTest { }
@SpringJUnitConfig(classes = {
PostService.class,
CommentValidator.class,
PostSanitizer.class
})
public class PostServiceSpringTest { }
2020 EPAM Systems, Inc.
@SpringBootTest
The search algorithm works up from the package that
contains the test until it finds a
@SpringBootApplication or
@SpringBootConfiguration annotated class. As long as
you’ve structured your code in a sensible way your main
configuration is usually found.
https://docs.spring.io/spring-
boot/docs/1.5.2.RELEASE/reference/html/boot-features-
testing.html#boot-features-testing-spring-boot-
applications-detecting-config
22
2020 EPAM Systems, Inc.
@SpringBootTest
23
Looks for
@SpringBootApplication or
@SpringBootConfiguration
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(…)
public @interface SpringBootApplication { }
@SpringBootConfiguration
@Configuration
@TestConfiguration
PostControllerSpringBootTest
2020 EPAM Systems, Inc.
@Sql, @SqlConfig and JdbcTestUtils
24
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class PostControllerWithDbInitTest {
}
2020 EPAM Systems, Inc.
@Sql, @SqlConfig and JdbcTestUtils
25
@Sql(
scripts = "/create_posts.sql",
config = @SqlConfig(separator = ";")
)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class PostControllerWithDbInitTest {
}
2020 EPAM Systems, Inc.
@Sql, @SqlConfig and JdbcTestUtils
26
@Sql(
scripts = "/create_posts.sql",
config = @SqlConfig(separator = ";")
)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class PostControllerWithDbInitTest {
@Test
@Sql("/create_special_post.sql")
void findOne_shouldFindSpecialPost() {
// ...
}
}
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScripts(new ClassPathResource("test-schema.sql"));
populator.setSeparator("@@");
populator.execute(this.dataSource);
2020 EPAM Systems, Inc.
@Sql, @SqlConfig and JdbcTestUtils
27
@Sql(
scripts = "/create_posts.sql",
config = @SqlConfig(separator = ";")
)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class PostControllerWithDbInitTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
@Sql("/create_special_post.sql")
void findOne_shouldFindSpecialPost() {
final int postCount = JdbcTestUtils.countRowsInTable(jdbcTemplate, "POSTS");
assertTrue(postCount > 0);
// ...
}
}
PostControllerWithDbInitTest
2020 EPAM Systems, Inc.
Testing The Blog Application
28
Post Controller Post Service Post Repository Post Database
REST client
2020 EPAM Systems, Inc.
Test layers - @JsonTest
• @JsonTest:
• CacheAutoConfiguration
• GsonAutoConfiguration
• JacksonAutoConfiguration
• JsonTestAutoConfiguration
29
PostControllerJsonTest
2020 EPAM Systems, Inc.
Testing The Blog Application
30
Post Controller Post Service Post Repository Post Database
Web Client
2020 EPAM Systems, Inc.
Test layers - @WebMvcTest
• @WebMvcTest:
• CacheAutoConfiguration
• MessageSourceAutoConfiguration
• HypermediaAutoConfiguration
• JacksonAutoConfiguration
• ThymeleafAutoConfiguration (*)
• ValidationAutoConfiguration
• ErrorMvcAutConfiguration
• HttpMessageConvertersAutoConfiguration
• ServerPropertiesAutoConfiguration
• WebMvcAutoConfiguration
• MockMvc(*)AutoConfiguration
31
PostControllerWebMvcTest, AdminControllerHtmlTest
2020 EPAM Systems, Inc.
Testing The Blog Application
32
Post Controller Post Service Post Repository Post Database
2020 EPAM Systems, Inc.
Test layers - @DataJpaTest
• @DataJpaTest:
• CacheAutoConfiguration
• JpaRepositoriesAutoConfiguration
• FlywayAutoConfiguration
• DataSourceAutoConfiguration
• DataSourceTransactionManagerAC
• JdbcTemplateAutoConfiguration
• HibernateJpaAutoConfiguration
• TransactionAutoConfiguration
• TestDatabaseAutoConfiguration
• TestEntityManagerAutoConfiguration
33
PostRepositoryDataJpaTest
2020 EPAM Systems, Inc.
Testing The Blog Application
34
Post Controller Post Service Post Repository Post Database
2020 EPAM Systems, Inc.
Test layers - @JdbcTest
• @JdbcTest:
• CacheAutoConfiguration
• FlywayAutoConfiguration
• DataSourceAutoConfiguration
• DataSourceTransactionManagerAC
• JdbcTemplateAutoConfiguration
• TransactionAutoConfiguration
• TestDatabaseAutoConfiguration
35
PostJdbcTest
2020 EPAM Systems, Inc.
Testing The Blog Application
36
Post Controller Post Service Post Repository Post Database
External REST
Service
2020 EPAM Systems, Inc.
Test layers - @RestClientTest
• @RestClientTest:
• CacheAutoConfiguration
• JacksonAutoConfiguration
• HttpMessageConverterAutoConfiguration
• WebClientAutoConfiguration
• MockRestServiceServerAutoConfiguration
• WebClientRestTemplateAutoConfiguration
37
PostImporterRestClientTest
2020 EPAM Systems, Inc.
Testing The Blog Application
38
Post Controller Post Service Post Repository
Mock or in-
memory
database
2020 EPAM Systems, Inc.
Docker Container
Testing The Blog Application
39
Post Controller Post Service Post Repository
Real DB
instance
Mock or in-
memory
database
2020 EPAM Systems, Inc.
TestContainers
• Integration tests with real dependencies in Docker
containers instead of mocks:
• Databases
• Message queues
• Browsers
• Anything else that could be run in Docker
• https://www.testcontainers.org/
40
PostServiceTestContainersTest
2020 EPAM Systems, Inc.
Examples weren’t shown
• @DertiesContext
• @ActiveProfiles
• @ContextHierarchy
• ReflectionTestUtils
• EnvironmentTestUtils
• Spring Cloud Contract
• Spring Cloud Stream Test
41
2020 EPAM Systems, Inc.
Conclusion
• Follow the Test Pyramid approach
• Use FIRST for tests
• Use SOLID for your code
• Spring Framework has a lot of tools that simplify
testing – use them
• https://github.com/aabarmin/epam-spring-testing
• https://docs.spring.io/spring/docs/current/spring-
framework-reference/testing.html
• https://docs.spring.io/spring-
boot/docs/1.5.2.RELEASE/reference/html/boot-
features-testing.html
• https://www.testcontainers.org/
• https://spring.io/projects/spring-cloud-contract
• https://cloud.spring.io/spring-cloud-static/spring-
cloud-
stream/2.1.3.RELEASE/multi/multi__testing.html
42
Thank you!
CONFIDENTIAL | © 2019 EPAM Systems, Inc.
QUESTIONS?
43

Тестирование Spring-based приложений

  • 1.
  • 2.
    2020 EPAM Systems,Inc. • Lead Software Engineer • EPAM Lab Mentor ALEKSANDR BARMIN • Email: Aleksandr_Barmin@epam.com • Twitter: @AlexBarmin CONTACTS 2
  • 3.
    2020 EPAM Systems,Inc. Agenda 1 6 2 3 4 5 W H Y T E S T I N G I S S O I M P O R T A N T C O N F I G U R I N G C O N T E X T F O R T E S T S U S I N G R E A L D E P E N D E N C I E S E X A M P L E S 3 O V E R V I E W O F T E S T I N G T E S T L A Y E R S
  • 4.
    2020 EPAM Systems,Inc. Overview of testing • A test case is a set of test inputs, execution conditions, and expected results developed for a particular objective, such as to exercise a particular program path or to verify compliance with a specific requirement. • https://en.wikipedia.org/wiki/Test_case 4
  • 5.
    2020 EPAM Systems,Inc. Test Suite Overview of testing 5 Test Test Test case System Under Test (SUT) Verifies behavior of
  • 6.
    2020 EPAM Systems,Inc. Overview of testing 6 Test runner Test class Executes Test method Test method Test method Teardown Verify Execute Setup Fixture System Under Test (SUT) Configures Restores Interact
  • 7.
    2020 EPAM Systems,Inc. Overview of testing 7 Order Controller Order Service Order Data Access Object Orders Database How to test it in isolation?
  • 8.
    2020 EPAM Systems,Inc. Overview of testing 8 Slow, complex test System Under Test (SUT) Dependency Tests Fast, simple test System Under Test (SUT) Test Double Tests Replaced with
  • 9.
    2020 EPAM Systems,Inc. Why testing is so important – Test Pyramid 9 End-to- end Component Integration UnitTest the business logic Verify that a service communicates with its dependencies Acceptance tests for a service Acceptance tests for an application Slow, brittle, costly Fast, reliable, cheap
  • 10.
    2020 EPAM Systems,Inc. Why testing is so important – the Ice Cream Cone 10 End-to-end Component Integration UnitTest the business logic Verify that a service communicates with its dependencies Acceptance tests for a service Acceptance tests for an application Slow, brittle, costly Fast, reliable, cheap
  • 11.
    2020 EPAM Systems,Inc. Deployment pipeline Why testing is so important - The deployment pipeline 11 Pre-commit tests Commit test stage Integration tests stage Component tests stage Deploy stage Production environment Not production ready Production ready Fast feedback Slow feedback
  • 12.
    2020 EPAM Systems,Inc. Talk is cheap. Show me the code - Linus Torvalds 12
  • 13.
    2020 EPAM Systems,Inc. The Blog Application 13 Post Controller Post Service Post Repository Post Database
  • 14.
    2020 EPAM Systems,Inc. Testing The Blog Application 14 Post Controller Post Service Post Repository Post Database PostServiceSpringTest
  • 15.
    2020 EPAM Systems,Inc. @ContextConfiguration 15 public @interface ContextConfiguration { }
  • 16.
    2020 EPAM Systems,Inc. @ContextConfiguration 16 public @interface ContextConfiguration { // where to find bean definitions String[] value() default {}; String[] locations() default {}; Class<?>[] classes() default {}; }
  • 17.
    2020 EPAM Systems,Inc. @ContextConfiguration 17 public @interface ContextConfiguration { // where to find bean definitions String[] value() default {}; String[] locations() default {}; Class<?>[] classes() default {}; // how to initialize the Application Context Class<? extends ApplicationContextInitializer<?>>[] initializers() default {}; }
  • 18.
    2020 EPAM Systems,Inc. @ContextConfiguration 18 public @interface ContextConfiguration { // where to find bean definitions String[] value() default {}; String[] locations() default {}; Class<?>[] classes() default {}; // how to initialize the Application Context Class<? extends ApplicationContextInitializer<?>>[] initializers() default {}; // should context from parent classes be loaded boolean inheritLocations() default true; boolean inheritInitializers() default true; }
  • 19.
    2020 EPAM Systems,Inc. @ContextConfiguration 19 public @interface ContextConfiguration { // where to find bean definitions String[] value() default {}; String[] locations() default {}; Class<?>[] classes() default {}; // how to initialize the Application Context Class<? extends ApplicationContextInitializer<?>>[] initializers() default {}; // should context from parent classes be loaded boolean inheritLocations() default true; boolean inheritInitializers() default true; // what will read bean definitions Class<? extends ContextLoader> loader() default ContextLoader.class; }
  • 20.
    2020 EPAM Systems,Inc. @ContextConfiguration 20 public @interface ContextConfiguration { // where to find bean definitions String[] value() default {}; String[] locations() default {}; Class<?>[] classes() default {}; // how to initialize the Application Context Class<? extends ApplicationContextInitializer<?>>[] initializers() default {}; // should context from parent classes be loaded boolean inheritLocations() default true; boolean inheritInitializers() default true; // what will read bean definitions Class<? extends ContextLoader> loader() default ContextLoader.class; // name of the context hierarchy level String name() default ""; }
  • 21.
    2020 EPAM Systems,Inc. @ContextConfiguration and @SpringJUnitConfig 21 @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = { PostService.class, CommentValidator.class, PostSanitizer.class }) public class PostServiceSpringTest { } @SpringJUnitConfig(classes = { PostService.class, CommentValidator.class, PostSanitizer.class }) public class PostServiceSpringTest { }
  • 22.
    2020 EPAM Systems,Inc. @SpringBootTest The search algorithm works up from the package that contains the test until it finds a @SpringBootApplication or @SpringBootConfiguration annotated class. As long as you’ve structured your code in a sensible way your main configuration is usually found. https://docs.spring.io/spring- boot/docs/1.5.2.RELEASE/reference/html/boot-features- testing.html#boot-features-testing-spring-boot- applications-detecting-config 22
  • 23.
    2020 EPAM Systems,Inc. @SpringBootTest 23 Looks for @SpringBootApplication or @SpringBootConfiguration @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(…) public @interface SpringBootApplication { } @SpringBootConfiguration @Configuration @TestConfiguration PostControllerSpringBootTest
  • 24.
    2020 EPAM Systems,Inc. @Sql, @SqlConfig and JdbcTestUtils 24 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class PostControllerWithDbInitTest { }
  • 25.
    2020 EPAM Systems,Inc. @Sql, @SqlConfig and JdbcTestUtils 25 @Sql( scripts = "/create_posts.sql", config = @SqlConfig(separator = ";") ) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class PostControllerWithDbInitTest { }
  • 26.
    2020 EPAM Systems,Inc. @Sql, @SqlConfig and JdbcTestUtils 26 @Sql( scripts = "/create_posts.sql", config = @SqlConfig(separator = ";") ) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class PostControllerWithDbInitTest { @Test @Sql("/create_special_post.sql") void findOne_shouldFindSpecialPost() { // ... } } ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.addScripts(new ClassPathResource("test-schema.sql")); populator.setSeparator("@@"); populator.execute(this.dataSource);
  • 27.
    2020 EPAM Systems,Inc. @Sql, @SqlConfig and JdbcTestUtils 27 @Sql( scripts = "/create_posts.sql", config = @SqlConfig(separator = ";") ) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class PostControllerWithDbInitTest { @Autowired private JdbcTemplate jdbcTemplate; @Test @Sql("/create_special_post.sql") void findOne_shouldFindSpecialPost() { final int postCount = JdbcTestUtils.countRowsInTable(jdbcTemplate, "POSTS"); assertTrue(postCount > 0); // ... } } PostControllerWithDbInitTest
  • 28.
    2020 EPAM Systems,Inc. Testing The Blog Application 28 Post Controller Post Service Post Repository Post Database REST client
  • 29.
    2020 EPAM Systems,Inc. Test layers - @JsonTest • @JsonTest: • CacheAutoConfiguration • GsonAutoConfiguration • JacksonAutoConfiguration • JsonTestAutoConfiguration 29 PostControllerJsonTest
  • 30.
    2020 EPAM Systems,Inc. Testing The Blog Application 30 Post Controller Post Service Post Repository Post Database Web Client
  • 31.
    2020 EPAM Systems,Inc. Test layers - @WebMvcTest • @WebMvcTest: • CacheAutoConfiguration • MessageSourceAutoConfiguration • HypermediaAutoConfiguration • JacksonAutoConfiguration • ThymeleafAutoConfiguration (*) • ValidationAutoConfiguration • ErrorMvcAutConfiguration • HttpMessageConvertersAutoConfiguration • ServerPropertiesAutoConfiguration • WebMvcAutoConfiguration • MockMvc(*)AutoConfiguration 31 PostControllerWebMvcTest, AdminControllerHtmlTest
  • 32.
    2020 EPAM Systems,Inc. Testing The Blog Application 32 Post Controller Post Service Post Repository Post Database
  • 33.
    2020 EPAM Systems,Inc. Test layers - @DataJpaTest • @DataJpaTest: • CacheAutoConfiguration • JpaRepositoriesAutoConfiguration • FlywayAutoConfiguration • DataSourceAutoConfiguration • DataSourceTransactionManagerAC • JdbcTemplateAutoConfiguration • HibernateJpaAutoConfiguration • TransactionAutoConfiguration • TestDatabaseAutoConfiguration • TestEntityManagerAutoConfiguration 33 PostRepositoryDataJpaTest
  • 34.
    2020 EPAM Systems,Inc. Testing The Blog Application 34 Post Controller Post Service Post Repository Post Database
  • 35.
    2020 EPAM Systems,Inc. Test layers - @JdbcTest • @JdbcTest: • CacheAutoConfiguration • FlywayAutoConfiguration • DataSourceAutoConfiguration • DataSourceTransactionManagerAC • JdbcTemplateAutoConfiguration • TransactionAutoConfiguration • TestDatabaseAutoConfiguration 35 PostJdbcTest
  • 36.
    2020 EPAM Systems,Inc. Testing The Blog Application 36 Post Controller Post Service Post Repository Post Database External REST Service
  • 37.
    2020 EPAM Systems,Inc. Test layers - @RestClientTest • @RestClientTest: • CacheAutoConfiguration • JacksonAutoConfiguration • HttpMessageConverterAutoConfiguration • WebClientAutoConfiguration • MockRestServiceServerAutoConfiguration • WebClientRestTemplateAutoConfiguration 37 PostImporterRestClientTest
  • 38.
    2020 EPAM Systems,Inc. Testing The Blog Application 38 Post Controller Post Service Post Repository Mock or in- memory database
  • 39.
    2020 EPAM Systems,Inc. Docker Container Testing The Blog Application 39 Post Controller Post Service Post Repository Real DB instance Mock or in- memory database
  • 40.
    2020 EPAM Systems,Inc. TestContainers • Integration tests with real dependencies in Docker containers instead of mocks: • Databases • Message queues • Browsers • Anything else that could be run in Docker • https://www.testcontainers.org/ 40 PostServiceTestContainersTest
  • 41.
    2020 EPAM Systems,Inc. Examples weren’t shown • @DertiesContext • @ActiveProfiles • @ContextHierarchy • ReflectionTestUtils • EnvironmentTestUtils • Spring Cloud Contract • Spring Cloud Stream Test 41
  • 42.
    2020 EPAM Systems,Inc. Conclusion • Follow the Test Pyramid approach • Use FIRST for tests • Use SOLID for your code • Spring Framework has a lot of tools that simplify testing – use them • https://github.com/aabarmin/epam-spring-testing • https://docs.spring.io/spring/docs/current/spring- framework-reference/testing.html • https://docs.spring.io/spring- boot/docs/1.5.2.RELEASE/reference/html/boot- features-testing.html • https://www.testcontainers.org/ • https://spring.io/projects/spring-cloud-contract • https://cloud.spring.io/spring-cloud-static/spring- cloud- stream/2.1.3.RELEASE/multi/multi__testing.html 42 Thank you!
  • 43.
    CONFIDENTIAL | ©2019 EPAM Systems, Inc. QUESTIONS? 43