Еще один длинный, но, надеюсь, полезный пост :) Пригодится тем, кто хочет начать писать unit-тесты, уже пишет их и хочет увеличить свои познания, и даже тем, кто тесты не пишет, но хочет быть в курсе того, что же это за зверь такой страшный - unit-тестирование.
Как я уже писал в предыдущем посте об unit-тестировании, иногда, для того, чтобы протестировать какой-нибудь кусок кода (например, метод), нужно довольно сильно постараться. Причем, это еще не тот вид извращений, когда вы тестируете методы UI, проблемы могут начаться с тестирования бизнес-логики. Дело в том, что очень часто тестируемый метод может вызывать методы других классов, которые в данном случае тестировать не нужно. Unit-тест потому и называется модульным, что тестирует отдельные модули, а не их взаимодействие. Причем, чем меньше тестируемый модуль – тем лучше с точки зрения будущей поддержки тестов. Для тестирования взаимодействия используются интеграционные тесты, где вы уже тестируете скорее полные use cases, а не отдельную функциональность.
Однако наши классы очень часто используют другие классы в своей работе. Например, слой бизнес логики (Business Logic layer) часто работает с другими объектами бизнес логики или обращается к слою доступа к данным (Data Access layer). В трехслойной архитектуре веб-приложений это вообще постоянный процесс: Presentation layer обращается к Business Logic layer, тот, в свою очередь, к Data Access layer, а Data Access layer – к базе данных. Как же тестировать подобный код, если вызов одного метода влечет за собой цепочку вплоть до базы данных?
В таких случаях на помощь приходят так называемые mock-объекты, предназначенные для симуляции поведения реальных объектов во время тестирования. Вообще, понятие mock-объект достаточно широко: оно может, с одной стороны, обозначать любые тест-дублеры (Test Doubles) или конкретный вид этих дублеров – mock-объекты. Я постараюсь использовать этот термин исключительно во втором случае, чтобы никого не путать, и не запутаться самому :)
Понятие тест-дублеров введено неким Gerard Meszaros в своей книге «XUnit Test Patterns» и теперь с подачи небезызвестного Мартина Фаулера эта терминология набирает популярность. Джерард и Мартин делят все тест-дублеры на 4 группы:
- Dummy – пустые объекты, которые передаются в вызываемые внутренние методы, но не используются. Предназначены лишь для заполнения параметров методов.
- Fake – объекты, имеющие работающие реализации, но в таком виде, который делает их неподходящими для production-кода (например, In Memory Database).
- Stub – объекты, которые предоставляют заранее заготовленные ответы на вызовы во время выполнения теста и обычно не отвечающие ни на какие другие вызовы, которые не требуются в тесте. Также могут запоминать какую-то дополнительную информацию о количестве вызовов, параметрах и возвращать их потом тесту для проверки.
- Mock – объекты, которые заменяют реальный объект в условиях теста и позволяют проверять вызовы своих членов как часть системы или unit-теста. Содержат заранее запрограммированные ожидания вызовов, которые они ожидают получить. Применяются в основном для т.н. interaction (behavioral) testing.
Поначалу эта классификация выглядит очень непонятной. Но если вдуматься, то можно разобраться, в чем заключается отличие между теми и иными типами объектов. Предположим, что вам нужно протестировать метод Foo() класса TestFoo, который делает вызов другого метода Bar() класса TestBar. Предположим, что метод Bar() принимает какой-нибудь объект класса Bla в качестве параметра и потом ничего особого с ним не делает. В таком случае имеет смысл создать пустой объект Bla, передать его в класс TestFoo (сделать это можно при помощи широко применяемого паттерна Dependency Injection или каким-либо другим приемлемым способом), а затем уже Foo() при тестировании сам вызовет метод TestBar.Bar() с переданным пустым объектом. Это и есть иллюстрация использования dummy-объекта в unit-тестировании.
К сожалению, редко можно обойтись простыми dummy-объектами. Иногда метод Bar() выполняет какие-то действия с ним (допустим, Bar() сохраняет данные в базу или вызывает веб-сервис, а мы этого не хотим). В таких случаях наш объект класса TestBar должен быть уже не таким глупым. Мы должны научить его в ответ на запрос сохранения данных просто выполнить какой-то простой код (допустим, сохранение во внутреннюю коллекцию). В таких случаях можно выделить интерфейс ITestBar, который будет реализовывать класс TestBar и наш дополнительный класс FakeBar. При unit-тестировании мы просто будем создавать объект класса FakeBar и передавать его в класс с методом Foo() через интерфейс. Естественно, при этом класс Bar будет по-прежнему создаваться в реальном приложении, а FakeBar будет использован лишь в тестировании. Это иллюстрация fake-объекта.
Со stub- и mock-объектами все немного сложнее, хотя и здесь есть от чего отталкиваться. Stub-объекты (стабы) – это типичные заглушки. Они ничего полезного не делают и умеют лишь возвращать определенные данные в ответ на вызовы своих методов. В нашем примере стаб бы подменял класс TestBar и в ответ на вызов Bar() просто бы возвращал какие-то левые данные. При этом внутренняя реализация реального метода Bar() бы просто не вызывалась. Реализуется этот подход через интерфейс и создание дополнительного класса StubBar, либо просто через создание StubBar, который является унаследованным от TestBar. В принципе, реализация очень похожа на fake-объект с тем лишь исключением, что стаб ничего полезного, кроме постоянного возвращения каких-то константных данных не требует. Типичная заглушка. Стабам позволяется лишь сохранять у себя внутри какие-нибудь данные, удостоверяющие, что вызовы были произведены или содержащие копии переданных параметров, которые затем может проверить тест.
Mock-объект (мок), в свою очередь, является, грубо говоря, более умной реализацией заглушки, которая уже не просто возвращает предустановленные данные, но еще и записывает все вызовы, которые проходят через нее, чтобы вы могли дальше в unit-тесте проверить, что именно эти методы вот этих вот классов были вызваны тестируемым методом и именно в такой последовательности (хотя учет последовательности и строгость проверки, в принципе, настраиваемая вещь). То есть мы можем сделать мок MockFoo, который будет каким-то образом вызывать реальный метод Foo() класса TestFoo и затем смотреть, какие вызовы тот сделал. Или сделать мок MockBar и затем проверить, что при вызове метода Foo() реально произошел вызов метода Bar() с нужными нам параметрами. Не совсем понятно? :) Вам нужно знать еще кое-что о unit-тестировании, чтобы понять разницу.
Unit-тестирование условно делится на два подхода:
- state-based testing, в котором мы тестируем состояние объекта после прохождения unit-теста
- interaction (behavioral) testing, в котором мы тестируем взаимодействие между объектами, поведение тестируемого метода, последовательность вызовов методов и их параметры и т.д.
То есть в state-based testing нас интересует в основном, в какое состояние перешел объект после вызова тестируемого метода, или, что более часто встречается, что в реальности вернул наш метод и правилен ли этот результат. Подобные проверки проводятся при помощи вызова методов класса Assert различных unit-тест фреймворков: Assert.AreEqual(), Assert.That(), Assert.IsNull() и т.д.
В interaction testing нас интересует прежде всего не статическое состояние объекта, а те динамические вызовы методов, которые происходят у него внутри. То есть для нашего примера с классами TestFoo и TestBar мы будем проверять, что тестируемый метод Foo() действительно вызвал метод Bar() класса TestBar, а не то, что он при этом вернул и в какое состояние перешел. Как правило, в случае подобного тестирования программисты используют специальные mock-фреймворки (TypeMock.Net, EasyMock.Net, MoQ, Rhino Mocks, NMock2), которые содержат определенные конструкции для записи ожиданий и их последующей проверки через методы Verify(), VerifyAll(), VerifyAllExpectations() или других (в зависимости от конкретного фреймворка).
То есть во многом это отличие можно назвать аналогичным отличию state machine diagram и activity diagram в UML: описывают они, в принципе, одно и то же, но разными способами. Иногда удобнее один, иногда второй.
Фаулер вот называет эти два подхода классическим (classical) и мокистским (mockist, ну и слово выдумал) unit-тестированием и делит программистов на тех, кто предпочитают первый и кто предпочитает второй подходы. Я бы этого не делал. Мне кажется, что иногда просто удобнее проверить состояние объекта, а иногда – его взаимодействие с другими объектами. Поэтому эти два подхода прекрасно уживаются вместе, когда вы понимаете, о чем идет речь, и что именно вы хотите сейчас проверить. Так же, как уживаются в одном тесте моки и стабы.
Вот такие пирожки. Теперь несколько примеров, которые иллюстрируют различия между этими двумя подходами и использования стабов и моков. Я также покажу, зачем в реальности может понадобиться interaction-тестирование, чтобы вам было проще выбирать тот или иной вид в будущем. Примеры используют NUnit и Rhino Mocks, хотя на их месте с небольшим изменением синтаксиса может оказаться почти любая другая пара фреймворков.
Допустим, у нас есть несколько простых классов:
public class Order
{
public string ProductName { get; private set; }
public int Quantity { get; private set; }
public bool IsFilled { get; private set; }
public Order(string productName, int quantity)
{
ProductName = productName;
Quantity = quantity;
}
public void Fill(IWarehouse warehouse)
{
if (warehouse.HasInventory(ProductName, Quantity))
{
warehouse.Remove(ProductName, Quantity);
IsFilled = true;
}
}
}
public class Warehouse
{
private DataAccess db;
public Warehouse()
{
db = new DataAccess();
}
public virtual bool HasInventory(string productName, int quantity)
{
return db.HasInventory(productName, quantity);
}
public virtual void Remove(string productName, int quantity)
{
db.Remove(productName, quantity);
}
}
Вот такой набор классов. Класс Order обращается к классу Warehouse, а тот обращается к базе данных. Предположим, что мы тестируем метод Fill() класса Order. Вот пример тестирования с использованием стаба для state-based тестирования:
[Test]
public void TestFillingOrderWithRhinoStub()
{
Order order = new Order(Talisker, 50);
var stubUserRepository = MockRepository.GenerateStub<Warehouse>();
stubUserRepository.Stub(x => x.HasInventory(Talisker, 50)).Return(true);
stubUserRepository.Stub(x => x.Remove(Talisker, 50));
order.Fill(stubUserRepository);
Assert.IsTrue(order.IsFilled);
}
Пара пояснений по коду. Сначала мы создаем объект типа Order, затем – стаб для класса Warehouse. После этого мы при помощи mock-фреймворка говорим, что при вызове метода HasInventory с определенными параметрами этот метод должен нам вернуть true. Аналогичным образом переопределяем поведение метода Remove (а то еще вызовет реальный и будет бяка). Далее идет вызов метода Fill() с переданным стабом, после чего проверяется, что свойство IsFilled установлено в true. Как видите, ничего сложного. Однако данный тест обладает некоторыми недостатками. Во-первых, непонятно, что делать, если в тестируемом объекте нет свойства, аналогичного IsFilled. Как проверять правильность выполнения кода? Во-вторых, непонятно, что случится, если программист удалит или закомментирует вызов следующей строчки в коде метода Fill():
warehouse.Remove(ProductName, Quantity);
IsFilled устанавливается в true, тест проходит, но код-то уже не работает!
Обе эти проблемы легко разрешаются, если мы воспользуемся interaction тестированием с использованием мока. Для этого напишем другой тест:
[Test]
public void TestFillingOrderWithRhino()
{
Order order = new Order(Talisker, 50);
var mockUserRepository = MockRepository.GenerateMock<Warehouse>();
mockUserRepository.Expect(x => x.HasInventory(Talisker, 50)).Return(true);
mockUserRepository.Expect(x => x.Remove(Talisker, 50));
mockUserRepository.Replay();
order.Fill(mockUserRepository);
Assert.IsTrue(order.IsFilled);
mockUserRepository.VerifyAllExpectations();
}
Начало теста аналогичное, затем идет создание мока Warehouse, после чего идет несколько вызовов метода Expect с теми же параметрами, что и в предыдущем тесте. При помощи этого метода мы говорим моку, что мы ожидаем вызова этих методов с такими параметрами и нам в ответ на их вызовы нужно вернуть такие-то значения. Затем идет вызов метода Replay(), который переводит мок из режима записи ожиданий в режим их проверки, то есть запуска тестового метода. Все моки имеют несколько режимов работы (Record, Replay, Verify), это распространенный подход. Далее непосредственно запуск, проверка IsFilled и вызов нового для нас метода VerifyAllExpectations(). Последний как раз и делает всю работу по проверке вызовов методов, параметров и т.д. Теперь, если метод Remove оказался закомментированным, тест не пройдет. Кроме того, нам уже не так важна проверка состояния объекта Order. Если бы свойства IsFilled не было, ничего бы не изменилось, а так мы лишь проверяем, что оно было установлено в соответствии с алгоритмом. Теперь немного поэкспериментируем с кодом. Что, если мы уберем второй Expect или поменяем их местами? Есть несколько режимов строгости проверки, которые задаются через конструктор класса Mock, который также можно использовать для создания мока. В Rhino Mocks есть три уровня строгости: Loose, Strict и Default (Loose). В Loose-режиме мок проверяет лишь то, что все ожидаемые методы были вызваны из тестируемого метода, в то время как в Strict-режиме проверяется также, что не было любых других вызовов и что порядок вызову соответствует порядку ожиданий. В других фреймворках иногда есть и другие режимы. Таким образом, в нашем случае при изменении порядка тест бы прошел, но в Strict-режиме – уже нет. Еще один момент, который показывает отличие методов Expect от методов Stub (в моке они также доступны): методы, зарегистрированные в моке при помощи метода Stub невидимы для метода VerifyAllExpectations. То есть, если нужна проверка вызовов – используйте Expect. Также стоит отметить, что при помощи дополнительных методов типа Return вы можете не только указывать возвращаемые значения, но еще генерировать exception'ы (Throw), вызывать настоящий метод (CallOriginalMethod), задавать ограничения на параметры (Constraints), вызывать дополнительные методы (Callback, Do), работать со свойствами и событиями. В общем, список потрясающий.
>>interaction testing, в котором мы тестируем
ReplyDeleteпервый раз такое слышу, обычно это называеться behaviral testing...
Я слышал оба названия, они используются взаимозаменяемо. Добавлю в текст для избежания недоразумений.
ReplyDeleteспасибо! многое стало понятным.
ReplyDeleteОгромное спасибо за этот исчерпывающий пост.
ReplyDeleteДумаю что это один из самых удачных постов в рунете, который посвящен теме mocking.
Когда первый раз его прочитал, то как-то не отблагодарил.
А вот уже второй раз на него натыкаюсь, причем в очень необычном контексте.
Сейчас пишу диплом. И хотел в дипломе выделить небольшой раздел, который посвящен мокингу. Ну и как любой настоящий студент искал материалы, которые с минимальными правками можно откопипастить в пояснительную записку :)
Надеюсь что ты не против того, чтобы я этот пост, с некоторой шлифовкой использовал повторно так сказать. Обязуюсь упомянуть твой блог в списке литературы ;)
Конечно, не против :) И тебе спасибо на добром слове.
ReplyDeleteОгромное спасибо, теперь всё стало на свои места :)
ReplyDeleteБольшое спасибо за понятное объяснение, некоторые детали до сих пор не понимал.
ReplyDeleteСпасибо.
ReplyDelete>> Дело в том, что очень часто тестируемый метод может вызывать методы других классов, которые в данном случае тестировать не нужно.
ReplyDeleteА якщо "тестируемый метод" викливає методи цього ж класу, які не потрібно тестувати (або вже простестовані)?
Наприклад, метод A викликає B, а B викливає методи C і D:
A -> B -> C D
Нам потрібно написати тести для A і B. Як створити заглушки спочатку C і D (в тестах для B) і тоді заглушку B (в тестах A)?
По первому вопросу: В идеальном случае в этом случае вы можете "замочить" и методы своего класса, которые вызываются из тестируемого. Это нормально, т.к. те методы будут другими тестами. Однако иногда имеет смысл не делать этого, а оставить вызовы внутренних методов класса (например, в случае приватных методов). Так что это не догма, решать вам.
ReplyDeleteПо второму вопросу (A -> B -> C D): Это тоже обычная ситуация. Мок-фреймворки позволяют спокойно мокать методы C и D для тестирования метода B, а потом точно так же метод B для тестирования метода A (при этом A вообще не знает, что в B вызываются еще какие-то методы C и D.
Дякую за відповіді і за наводку: пропертя CallBase класу MockRepository - те, що шукав (NUnit).
ReplyDeleteДа, присоединюсь к Дияну, поблагодарю тебя за этот пост! Хотя в общем то, я искал документацию по NMocks, но пост все равно оказался полезным.
ReplyDeleteВсе равно уже решил использовать Rhino.
Ксати, насчет контроля поведения и состояния.
Если использовать ожидание изменения свойства - то это тоже в какой-то степени контроль состояний.
Просто у нас есть некий удобный фреймвёрк для того, чтобы каждый раз не мучаться с рефлексией.
Спасибо большое за пост! Очень понятно всё описано! Побольше бы таких постов в интернете...
ReplyDeleteСпасибо Александру за отличные статьи!
ReplyDeleteА если у меня, например, объект Warehouse создается внутри метода Fill (абстрактный случай, конечно), то как тогда тестировать этот метод? в данном примере мы просто передаем в order.Fill() наш стаб.
ReplyDeleteЗдесь нужно применять один из подходов Dependency Injection (Constructor Injection, Method Injection, Property Injection). То есть прокидывать готовый объект Warehouse (как правило, через интерфейсную ссылку) либо в конструктор (предпочтительнее), либо в метод, либо в свойство (редко используется). Выбор конструктор-или-метод зависит от ситуации, если объект нужен лишь внутри этого конкретного метода и объект будет создаваться вручную - лучше в метод, если это какие-то репозитории или сервисы, которые проще создавать автоматически - лучше в конструктор. Для автоматического прокидывания берем любой IoC контейнер: Autofac, Ninject, Unity, etc. А потом конфигурируем IoC, либо используем Mock фреймворки так, что в режиме тестирования в метод идут стабы/моки, а в режиме выполнения - реальные объекты.
ReplyDeleteТо есть основная идея - делегировать создание реального объекта и принятие решения о том, что именно создавать кому-нибудь наверх, кто знает, в каком режиме мы сейчас запущены (тест или выполнение).
Еще можно использовать паттерн Service Locator, но сейчас этот подход используется намного реже.
Всем спасибо за спасибо :) Рад, что материал актуален до сих пор, через 4 года.
ReplyDeleteСпасибо. а как в данном случае (в вашем примере) можно это сделать? Method Injection. Если использовать Unity. просто для меня это еще "темные материи" :)
ReplyDeleteВ IoC контейнерах обычно используется Constructor и Property Injection. Если вам нужен Method injection через IoC, возможно, вы что-то делаете не так. Вызовы методов обычно контролируются на уровне кода, поэтому автоматические иньекции там не нужно. Но тут вроде написано, как сделать Method injection в Unity: http://stackoverflow.com/questions/515028/setter-property-injection-in-unity-without-attributes. Как говорится, если очень хочется, то можно :)
ReplyDeleteЗамечательная статья! Спасибо вам большое!
ReplyDeleteВот уже наступил 2013-й год, а статья до сих пор поддерживает статус "зашибись"!
ReplyDeleteСпасибо большое за статью, вот бы где еще найти зачетный код-конвеншн для юнит-тестирования...
This comment has been removed by the author.
ReplyDeleteХорошая статья, единственный вопрос остался c вызовом конструкторов в тестируемых методов, как создать моки этих новых сущностей?.
ReplyDeleteЕсли я правильно понял вопрос, то в идеале вы должны передавать сущности, которые "должны быть созданы в тестируемых методах", извне (например, в тот же метод). Таким образом, вы создаете мок снаружи в тесте (либо реальный объект в коде), а потом передаете внутрь.
ReplyDeleteНапример:
ReplyDeleteJDBCHelper helper = new JDBCHelper();
ReferenceModel refModel = new ReferenceModel();
DataObject object = (DataObject) value.getValue();
...
ReferenceSelector refSelector = new ReferenceSelector();
refSelector.setURLPattern(ReferenceSelector.DEFAULT_PATTERN);
refSelector.setModel(model);
if (object == null) {
refSelector.setStartObjectId(trfContextHelper.getCustomerIdFromContext(retrieveContext()));
refSelector.setObjectTypeId(helper.getObjectTypeIdFromReferenceAttribute(value.getId()));
....
Проблема в том, что метод getObjectTypeIdFromReferenceAttribute вызывается, у объекта, который создан внутри метода, но т.к. половина других объектов заменены моками и параметр на вход также приходит тестовый то метод падает с NullPointer. Замочить этот метод тоже нельзя, т.к. вызывается он у вновь созданного объекта refSelector. первый раз я наверное неправильно сформулировал вопрос.
Насколько я понял, ReferenceSelector - это созданный вами класс, верно? В таком случае его объект нужно создавать снаружи данного метода и передавать внутрь. Тогда вы сможете его замочить без особых проблем. При необходимости объект можно передавать и через конструктор, используя возможности вашего IoC-контейнера. Та же история и с JDBCHelper.
ReplyDeleteВот только логика в таком случае немного усложняется, конечно. Однако это и есть инверсия зависимости, ваш метод перестает зависеть от создаваемых внутри объектов, и зависит от интерфейса или базового класса, позволяю вызывающему коду решать, что же именно передать внутрь.
По этому треду комментариев можно проследить эволюцию моих аватаров за 5 лет :D
ReplyDeleteСпасибо, очень хорошая статья. Добавить бы еще сравнение mock фреймворков, и моему счастью не было б предела)))
ReplyDeleteОтличная статья, даже через 5 лет:)
ReplyDeleteSerP, Изучаю эту тему сейчас, и по собранной статистике можно сказать, что Rhino Mocks самый популярный
Замечательная статья, я столько перерыл инфы, но везде все описано как-то сложно и непонятно. У тебя я сразу все понял)))) Спасибо огромное)))))
ReplyDeleteЛучшая статья на эту тему)
ReplyDeleteСпасибо автору за прекрасную и понятную статью, а также за ответы на вопросы. И спасибо всем за прекрасные вопросы в комментариях
ReplyDeleteПовторюсь, отличная статья! Автору respect!
ReplyDeleteСпасибо за статью, в 2016м помогаете новичкам как и в далёком 2008м.
ReplyDeleteХорошая статья, теперь все разложилось по полочкам. Спасибо!
ReplyDeleteПревосходно разъеснены моменты, в других источниках подразумевающиеся как само-собой очевидные. Спасибо!
ReplyDeleteспасибо, полезно
ReplyDeleteкупить семена адениума
ReplyDeleteЭкзотические растения свежиесемена адениума и другие комнатные цветы.
еще
Спасибо, текст до сих пор актуален, было полезно.
ReplyDeleteI know this if off topic but I'm looking into starting my own weblog and was wondering what all is required to get setup? I'm assuming having a blog like yours would cost a pretty penny? I'm not very web savvy so I'm not 100% certain. Any suggestions or advice would be greatly appreciated. Thank you
ReplyDeleteEskişehir
ReplyDeleteDenizli
Malatya
Diyarbakır
Kocaeli
7FQK4D
ankara parça eşya taşıma
ReplyDeletetakipçi satın al
antalya rent a car
antalya rent a car
ankara parça eşya taşıma
CWHN
adana evden eve nakliyat
ReplyDeletebolu evden eve nakliyat
diyarbakır evden eve nakliyat
sinop evden eve nakliyat
kilis evden eve nakliyat
DX7PQ
izmir evden eve nakliyat
ReplyDeletemalatya evden eve nakliyat
hatay evden eve nakliyat
kocaeli evden eve nakliyat
mersin evden eve nakliyat
HQYM
43195
ReplyDeleteİstanbul Parça Eşya Taşıma
Kilis Parça Eşya Taşıma
Mersin Lojistik
Bitlis Lojistik
Sivas Evden Eve Nakliyat
CC665
ReplyDeleteUrfa Evden Eve Nakliyat
Hakkari Şehir İçi Nakliyat
Çerkezköy Mutfak Dolabı
Rize Parça Eşya Taşıma
Bitget Güvenilir mi
İzmir Parça Eşya Taşıma
Batıkent Parke Ustası
Mamak Boya Ustası
Antalya Lojistik
E38CB
ReplyDeleteAdana Evden Eve Nakliyat
Düzce Şehir İçi Nakliyat
Tekirdağ Parça Eşya Taşıma
Kırklareli Şehir İçi Nakliyat
Batman Şehirler Arası Nakliyat
AAX Güvenilir mi
Sakarya Parça Eşya Taşıma
Kaspa Coin Hangi Borsada
Azero Coin Hangi Borsada
3127E
ReplyDeletebilecik görüntülü sohbet ücretsiz
elazığ canli goruntulu sohbet siteleri
agri rastgele sohbet odaları
kastamonu sesli sohbet sitesi
mobil sohbet
kocaeli canlı sohbet siteleri
adıyaman bedava sohbet odaları
konya kadınlarla görüntülü sohbet
canli goruntulu sohbet siteleri
18DB1
ReplyDeleteBolu Rastgele Görüntülü Sohbet Uygulaması
uşak canli sohbet chat
kırklareli rastgele sohbet siteleri
adıyaman görüntülü sohbet ücretsiz
Mardin Telefonda Rastgele Sohbet
Artvin Mobil Sohbet Sitesi
seslı sohbet sıtelerı
antalya sesli sohbet
burdur görüntülü sohbet uygulama
7CFBD
ReplyDeleteBone Coin Hangi Borsada
Tesla Coin Hangi Borsada
Kripto Para Madenciliği Nasıl Yapılır
Mefa Coin Hangi Borsada
Facebook Beğeni Hilesi
Bitcoin Üretme
Bitcoin Nasıl Oynanır
Clubhouse Takipçi Hilesi
Linkedin Takipçi Satın Al
92ABE
ReplyDeleteBinance Referans Kodu
Binance Referans Kodu
Gate io Borsası Güvenilir mi
Likee App Beğeni Hilesi
Binance Madenciliği Nedir
Coin Para Kazanma
Bitcoin Kazanma Siteleri
Facebook Beğeni Hilesi
Binance Ne Kadar Komisyon Alıyor
034D5
ReplyDeleteBitcoin Madenciliği Siteleri
Onlyfans Takipçi Satın Al
Bitcoin Nasıl Kazanılır
Cate Coin Hangi Borsada
Tiktok Takipçi Hilesi
Coin Çıkarma Siteleri
Bitcoin Kazma Siteleri
Kwai Takipçi Satın Al
Qlc Coin Hangi Borsada
60732
ReplyDeleteledger desktop
defillama
ellipal
defilama
shiba
dappradar
trezor suite
poocoin
avax
2390BD42C8
ReplyDeleteşov sitesi
99A3FC256E
ReplyDeletemedi finance
galxe stake
puffer finance
rocketpool
galxe
tokenfi
bitget
moonbeam
emoji coin
7BE8B6BD64
ReplyDeleteinstagram türk beğeni satın al