AWS: DynamoDB için Testlerin Yazılması
DynamoDB’nin Java API’sinin nasıl kullanılacağına önceki yazıda deÄŸindik fakat uygulamamızı çalıştırarak test ettik. Her ne kadar bu tip blog yazılarında kabul edilebilir olsa da ciddi bir uygulamayı bu ÅŸekilde test edemezsin. Bu yazıda, DynamoDB kullanan ciddi bir uygulamanın, DAL (Data Access Layer) katmanını nasıl test edebileceÄŸimize bakacağız.
Ne yazıkki, genel olarak şu şekilde yazın diyebileceğim bir konu değil. Her test stratejisinin farklı avantajları farklı dezavantajları var. Bu sebepten Integration Test ya da Unit Test diye ayırmadan farklı test stratejilerini inceleyip avantajlarına dezavantajlarına bakalım.
Doğruca AWS Üzerinden Test Edilmesi
Akla ilk gelen çözümlerden biri. Testlerinizin çalışacağı bir DynamoDB tablosu yaratıyorsunuz ve tüm testleriniz bu tablo üzerinden çalışıyor. Integration Test gibi düşünebilirsiniz. Eğer kapasitenizin limitlerini ölçmek istiyorsanız kullanabileceğiniz tek strateji bu.
Avantajları
- Uygulaması en kolay strateji. Sadece test veritabanınız için AmazonDynamoDBClient yaratmanız ve ilgili sınıflara sağlamanız yeterli oluyor.
- Load Test yapmak istiyorsanız, limitlerinizi ölçebileceğiniz tek strateji.
Dezavantajları
- Her ne kadar, Free Tier’da, 20 RCU ve 20 WCU hakkınız olsa da bu limiti testlerle aÅŸmak oldukça kolay. Bu sebepten bu yöntem en basit olduÄŸu kadarıyla da en maliyetli yöntem. Hatta tek maliyetli yöntem.
- Tüm takım aynı testlerde aynı tabloyu kullanıyorsa testlerinizi yazarken bunu göz önüne almanız lazım. Örneğin testlerinde sabit bir PartitionKey kullanıyorsanız, iki yazılımcı aynı projeyi derlediğinde, False Negative almaları bir hayli olası. Bunu her test için farklı tablo yaratıp/silerek aşabilirsiniz. Ama bu seferde maliyetinizi arttırma durumunuz var.
- Testleriniz için gerekli olan veriyi sizin kaydetmeniz gerekiyor. Aynı şekilde yazdığınız verileri temizleme işide size ait.
İstemcinin Mock’lanması
Diğer akla gelen çözüm, istemcilerin mocklanması. Kullandığınız DynamoDB istemcisi neyse onu Mockito ve benzeri kütüphaneler yardımıyla mockluyorsunuz.
Avantajları
- Her türlü durumu yaratmanız mümkün. Mesela kapasite aşımı yaptığınızda ProvisionedThroughputExceededException türünde bir hata alırsınız. Bu hatayı gerçek sunucu üzerinde gerçek veritabanını kullanarak oluşturmak çok zahmetli olabiliyor. Fakat istemcinizi mockladığınızda istediğiniz her hatayı rahatlıkla oluşturabilirsiniz.
- Diğer test stratejilerine göre çok daha hızlı. Ne Network işlemi yapıyorsunuz ne başka birşey.
- Gerçek bir kayıt işlemi yapmadığınız için testlerden sonra bir temizleme işlemi yapmanıza gerek yok.
- Testlerinizi yazarken diğer etkenlerden tamamen izole durumdasınız. Önceki stratejide olduğu gibi diğer yazılımcıları düşünmenize gerek yok. False Negative alma ihtimaliniz neredeyse yok.
Dezavantajları
- Aslında yine veriyi siz yaratıyorsunuz. Fakat bu sefer sadece yaratmakla kalmıyor, üzerine bu verileri kullandığımız istemcinin döneceÄŸi formata çevirmeniz gerekiyor. EÄŸer Java’nın High Level API’sini kullanıyorsanız, sadece nesne döneceksiniz, çok sorun yok. Ama Java’nın Low Level API’sini kullanıyorsanız, tüm Request ve Response türlerini bilip buna göre mock’lama yapmanız gerekiyor.
- Eğer özel Query ya da Scan sorgularınız varsa, ki ciddi bir uygulamada büyük ihtimalle olacaktır, bunların geçerli olup olmadığını bilemiyorsunuz. Bunların neler dönebileceğini belirtiyorsunuz. Ama gerçekten çalışıyor mu bilemiyorsunuz. Diğer test stratejilerinden birini kullanıp özel sorgularınızı test etmeniz gerekiyor.
Lokal DynamoDB Sunucusu Kullanmak
Önerebileceğim son test yöntemi Lokal DynamoDB sunucusu kullanmak. Eğer daha önce duymadıysanız, AWS geliştiricilerini düşünerek, Lokal DynamoDB sunucusu sağlıyor. Bu sunucuyu kullanarak tüm veritabanı operasyonlarınızı yapabilirsiniz. Her ne kadar ZIP formatında indirip çalıştırmak çok kolay olsa da test sırasında ayağa kaldırmak zahmetli olabiliyor. Bir sonraki bölümde nasıl gerçekleyeceğinize bakacağız ama önce diğer stratejilerdeki gibi avantajlara ve dezavantajlara bakalım.
Avantajları
- DoÄŸruca AWS üzerinden test edermiÅŸ gibi testlerinizi çalıştırabiliyorsunuz. Tek yapmanız gereken istemcinizin baÄŸlanacağı adresi AWS’ten Lokal’inize çevirmek.
- Tamemen ücretsiz
- Gerçek DynamoDB API’si üzerinden konuÅŸtuÄŸunuz için, lokal sunucuda çalışan tüm sorgularınız, AWS üzerinde ki sunucuda da çalışacaktır. Ek bir iÅŸlem yapmanıza gerek yok.
- Lokal DynamoDB’ye testleriniz için kayıt oluÅŸturmanız gerekiyor ama sunucunuz bellekte çalıştığından temizleme yapmanıza gerek yok.
- Testlerinizi yazarken diğer etkenlerden tamamen izole durumdasınız.
Dezavantajları
- Hemen hemen her türlü hata durumunu oluşturabilseniz de limit aşımı durumunu Lokal DynamoDB sunucusu kullanarak oluşturamıyor, test edemiyorsunuz.
- Alt tarafta SQLite kullanıyor. Kullandığı SQLite kütüphanesinden dolayı Native kütüphane bağımlılığı var. Bu sebepten kütüphane olarak kullanımı biraz uğraştırıyor.
- Her test için veri setinizi yaratmanız gerekiyor.
Örnek Kullanım
Kütüphane olarak kullanımı biraz uÄŸraÅŸtırdığından bir örnek kullanım da paylaÅŸmak istiyorum. Mock’lamayı ya da doÄŸrudan AWS kaynaklarını kullanırken, yapılan iÅŸlemlerin bir karmaşıklığı olmadığından onlarda örnek kullanım yazmaya ihtiyaç duymadım. EÄŸer bir sorun yaÅŸarsanız, yorum olarak paylaşırsanız elimden geldiÄŸince yardımcı olmaya çalışırım.
Örnek kullanımda, bir önceki yazıda yazdığımız UserRepositoryImpl sınıfının testlerini, Lokal DynamoDB sunucusu kullanarak yazacağım. Bunu için öncelikle maven’a aÅŸağıdaki bağımlılıkları ve eklentileri ekliyoruz.
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> ... <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.10</version> <executions> <execution> <id>copy</id> <phase>test-compile</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <includeScope>test</includeScope> <includeTypes>so,dll,dylib</includeTypes> <outputDirectory>${project.basedir}/native-libs</outputDirectory> </configuration> </execution> </executions> </plugin> </plugins> </build> <dependencies> ... <dependency> <groupId>com.amazonaws</groupId> <artifactId>DynamoDBLocal</artifactId> <version>1.11.0.1</version> <scope>test</scope> </dependency> <dependency> <groupId>com.almworks.sqlite4java</groupId> <artifactId>sqlite4java</artifactId> <version>1.0.392</version> <scope>test</scope> </dependency> <dependency> <groupId>com.almworks.sqlite4java</groupId> <artifactId>sqlite4java-win32-x86</artifactId> <version>1.0.392</version> <type>dll</type> <scope>test</scope> </dependency> <dependency> <groupId>com.almworks.sqlite4java</groupId> <artifactId>sqlite4java-win32-x64</artifactId> <version>1.0.392</version> <type>dll</type> <scope>test</scope> </dependency> <dependency> <groupId>com.almworks.sqlite4java</groupId> <artifactId>libsqlite4java-osx</artifactId> <version>1.0.392</version> <type>dylib</type> <scope>test</scope> </dependency> <dependency> <groupId>com.almworks.sqlite4java</groupId> <artifactId>libsqlite4java-linux-i386</artifactId> <version>1.0.392</version> <type>so</type> <scope>test</scope> </dependency> <dependency> <groupId>com.almworks.sqlite4java</groupId> <artifactId>libsqlite4java-linux-amd64</artifactId> <version>1.0.392</version> <type>so</type> <scope>test</scope> </dependency> </dependencies> <repositories> <repository> <id>dynamodblocal</id> <name>AWS DynamoDB Local Release Repository</name> <url>http://dynamodb-local.s3-website-us-west-2.amazonaws.com/release</url> </repository> </repositories> </project> |
Burada dikkatinizi çekmek istediğim nokta her bir işletim sistemi için Native kütüphaneleri tek tek belirtiyoruz. Daha sonra eklenti kısmında bu kütüphaneleri kolayca erişebileceğimiz bir yere, projenin ana dizinindeki native-libs klasörüne kopyalıyoruz. Testlerimizi yazmaya başladığımızda bu yolu kullanarak, Native kütüphaneleri sistemimize tanıtacağız.
Her DAO için sunucuyu ayağa kaldırmakla, veritabanını yaratmakla uğraşmayalım diye bir JUnit kuralı yazalım. Bu kural bizim için uygun bir port seçip sunucuyu ayağa kaldırsın ve gerekli tabloları model sınıfından yaratsın. Bunun için projemizin src/test/java klasöründe com.bahadirakin.dynamodb.rules paketinin içerisine LocalDynamoDBCreationRule isminde sınıfımızı yaratıyoruz.
LocalDynamoDBCreationRule.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | package com.bahadirakin.dynamodb.rules; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; import org.junit.rules.ExternalResource; import java.io.IOException; import java.net.ServerSocket; public class LocalDynamoDBCreationRule extends ExternalResource { private final Class<?>[] modelClasses; private DynamoDBProxyServer server; private AmazonDynamoDB amazonDynamoDB; private DynamoDBMapper dynamoDBMapper; public LocalDynamoDBCreationRule(final Class<?>... modelClasses) { this.modelClasses = modelClasses; // This one should be copied during test-compile time. If project's basedir does not contains a folder // named 'native-libs' please try '$ mvn clean install' from command line first System.setProperty("sqlite4java.library.path", "native-libs"); } @Override protected void before() throws Throwable { try { final String port = getAvailablePort(); this.server = ServerRunner.createServerFromCommandLineArgs(new String[]{"-inMemory", "-port", port}); server.start(); amazonDynamoDB = new AmazonDynamoDBClient(new BasicAWSCredentials("access", "secret")); amazonDynamoDB.setEndpoint("http://localhost:" + port); dynamoDBMapper = new DynamoDBMapper(amazonDynamoDB); for (Class<?> model : modelClasses) { final CreateTableRequest createTableRequest = dynamoDBMapper.generateCreateTableRequest(model); createTableRequest.setProvisionedThroughput(new ProvisionedThroughput(10L, 10L)); amazonDynamoDB.createTable(createTableRequest); } } catch (Exception e) { throw new RuntimeException(e); } } @Override protected void after() { if (server == null) { return; } try { server.stop(); } catch (Exception e) { throw new RuntimeException(e); } } public AmazonDynamoDB getAmazonDynamoDB() { return amazonDynamoDB; } public DynamoDBMapper getDynamoDBMapper() { return dynamoDBMapper; } private String getAvailablePort() { try (final ServerSocket serverSocket = new ServerSocket(0)) { return String.valueOf(serverSocket.getLocalPort()); } catch (IOException e) { throw new RuntimeException("Available port was not found", e); } } } |
Az önce belirttiğim gibi Constructor içerisinde native-libs klasörünü, sistem özellikleri üzerinden, SQLite kütüphanesine iletiyoruz. Ama dikkatinizi çekmek istediğim bir nokta daha var. Farkettiyseniz tablo yaratma isteklerini gönderirken kapasite kullanımını yine de belirtiyoruz. Bu az önce söylediklerimle çelişiyormuş gibi gelebilir. Burada ne değer belirtirseniz belirtin, hiç bir şekilde limit aşımı hatası almayacaksınız. Fakat bu değeri vermediğinizde, normal AWS servislerinde alacağınız hatanın aynısını alırsınız.
Son olarak ise bu kuralı testlerimizde nasıl kullanacağımıza bakalım. Bunun için projemizin src/test/java klasöründe com.bahadirakin.dynamodb.dao paketinin içerisine UserRepositoryImplTest isminde sınıfımızı yaratıyoruz.
UserRepositoryImplTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | package com.bahadirakin.dynamodb.dao; import com.bahadirakin.dynamodb.model.User; import com.bahadirakin.dynamodb.rules.LocalDynamoDBCreationRule; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.util.List; import java.util.UUID; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertThat; @RunWith(JUnit4.class) public class UserRepositoryImplTest { @ClassRule public static final LocalDynamoDBCreationRule dynamoDB = new LocalDynamoDBCreationRule(User.class); @Rule public final ExpectedException expectedException = ExpectedException.none(); private UserRepositoryImpl userDAO; @Before public void setUp() throws Exception { userDAO = new UserRepositoryImpl(dynamoDB.getDynamoDBMapper()); } @Test public void createdUserShouldBeReadable() throws Exception { // Given final User user = new User(); user.setUsername(UUID.randomUUID().toString()); user.setPassword(UUID.randomUUID().toString()); user.setEmail(UUID.randomUUID().toString()); // When userDAO.put(user); final User actualUser = userDAO.get(user.getUsername()); // Then assertThat(actualUser, is(equalTo(user))); } @Test public void createdUsersShouldBeDeletable() throws Exception { // Given final User user = new User(); user.setUsername(UUID.randomUUID().toString()); user.setPassword(UUID.randomUUID().toString()); user.setEmail(UUID.randomUUID().toString()); // When userDAO.put(user); userDAO.delete(user.getUsername()); final List<User> allUsers = userDAO.findAll(); // Then assertThat(allUsers, not(contains(user))); } @Test public void createdUsersShouldBeFindableUsingEmail() throws Exception { // Given final User user = new User(); user.setUsername(UUID.randomUUID().toString()); user.setPassword(UUID.randomUUID().toString()); user.setEmail(UUID.randomUUID().toString()); // When userDAO.put(user); final List<User> users = userDAO.findByEmail(user.getEmail()); // Then assertThat(users, both(hasSize(1)).and(contains(user))); } @Test public void getShouldThrowExceptionWhenUserWasNotFound() throws Exception { // Given final String username = UUID.randomUUID().toString(); // Then -- expected exception expectedException.expect(Exception.class); expectedException.expectMessage(String.format("User for username %s was not found", username)); // When userDAO.get(username); } } |
Peki sen hangisini tercih ediyorsun?
… Diye soracak olursanız, ben hepsinden gerektiÄŸi kadar kullanıyorum. Ama iÅŸin içine ücret kalemi girdiÄŸinden en son doÄŸrudan AWS servislerini kullanmayı seçiyorum. İlk tercihimse Lokal sunucu kullanmak oluyor. Tüm uç noktaları ise Mocklama yöntemiyle test ediyorum. Her ne kadar kendi Lokalimizde de çalıştırsak bazı uç durumları oluÅŸturmak gerçekten zor olabiliyor.
Git:Â https://github.com/bhdrkn/Java-Examples/tree/master/aws-dynamodb-tutorial