[Тестирование IT-систем, Go, Тестирование веб-сервисов] Подсказки по написанию тестов в приложениях на Go
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В нашей компании в стеке разработки есть язык Go. И иногда, при написании unit-тестов к приложениям написанным на Go, у нас появляются сложности. В этой статье мы расскажем о некоторых моментах, которые мы учитываем при написании тестов. На примерах разберём как их можно использовать. Используем интерфейсы при разработкеОб этом говорится в каждой статье про написании тестов, и эта не будет исключением. Интерфейсы уменьшают связанность между пакетами. Но главное их преимущество при написании тестов заключается в том, что интерфейсы позволяют мокать конкретные зависимости. Например, есть у нас функция, которая на основе содержимого Redis принимает какое-то решение о своей работе:
package yourpackage
import (
"context"
"github.com/go-redis/redis/v8"
)
func CheckLen(ctx context.Context, client *redis.Client, key string) bool {
val, err := client.Get(ctx, key).Result()
if err != nil {
return false
}
return len(val) < 10
}
Тесты для неё могут выглядеть примерно так:
package yourpackage
import (
"context"
"testing"
"github.com/go-redis/redis/v8"
)
func TestCheckLen(t *testing.T) {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
err := rdb.Set(ctx, "some_key", "value", 0).Err()
if err != nil {
t.Fatalf("redis return error: %s", err)
}
got := CheckLen(ctx, rdb, "some_key")
if !got {
t.Errorf("CheckLen return %v; want true", got)
}
}
Но как проверить ситуацию, когда Redis возвращает ошибку? Или что делать, если мы не хотим добавлять Redis в наш CI? То есть как нам замокать вызов Redis? И ответ на эти вопросы — используйте интерфейсы!Перепишем наш код с использованием интерфейсов:
package yourpackage
import (
"context"
"github.com/go-redis/redis/v8"
)
type Storage interface {
Set(ctx context.Context, key string, v interface{}) error
Get(ctx context.Context, key string) (string, error)
}
type RedisStorage struct {
Redis *redis.Client
}
func (rs *RedisStorage) Set(ctx context.Context, key string, v interface{}) error {
return rs.Redis.Set(ctx, key, v, 0).Err()
}
func (rs *RedisStorage) Get(ctx context.Context, key string) (string, error) {
return rs.Redis.Get(ctx, key).Result()
}
func CheckLen(ctx context.Context, storage Storage, key string) bool {
val, err := storage.Get(ctx, key)
if err != nil {
return false
}
return len(val) < 10
}
Интерфейсы не только упрощают написание тестов, но и в процессе разработки облегчают переход на другие технологии, например, замену Redis на Memcached. Тест же с использованием мока будет выглядеть примерно так:
package yourpackage
import (
"context"
"testing"
)
type testRedis struct{}
func (t *testRedis) Get(ctx context.Context, key string) (string, error) {
return "value", nil
}
func (t *testRedis) Set(ctx context.Context, key string, v interface{}) error {
return nil
}
func TestCheckLen(t *testing.T) {
ctx := context.Background()
storage := &testRedis{}
got := CheckLen(ctx, storage, "some_key")
if !got {
t.Errorf("CheckLen return %v; want true", got)
}
}
Используем генераторы моковПонятное дело, что для каждого случая писать свой мок немного избыточно. Можно попробовать написать универсальный мок. А можно попробовать его сгенерировать на основе интерфейса. Существует множество генераторов моков. Нам нравится https://github.com/vektra/mockery.Для примера выше написание тестов с использованием генератора могло бы выглядеть следующим образом. Сначала сгенерируем мок для нашего интерфейса:
mockery --recursive=true --inpackage --name=Storage
И дальше используем его в тестах следующим образом:
package yourpackage
import (
"context"
"testing"
mock "github.com/stretchr/testify/mock"
)
func TestCheckLen(t *testing.T) {
ctx := context.Background()
storage := new(MockStorage)
storage.On("Get", mock.Anything, "some_key").Return("value", nil)
got := CheckLen(ctx, storage, "some_key")
if !got {
t.Errorf("CheckLen return %v; want true", got)
}
Перехватываем логированиеДопустим у нас есть код, который логирует свои действия с использованием какой-либо сторонней библиотеки, например, Logrus.
package yourpackage
import (
log "github.com/sirupsen/logrus"
)
func Minus(a, b int) int {
log.Infof("Minus(%v, %v)", a, b)
return a - b
}
func Plus(a, b int) int {
log.Infof("Plus(%v, %v)", a, b)
return a + b
}
func Mul(a, b int) int {
log.Infof("Mul(%v, %v)", a, b)
return a + b // тут ошибка
}
И тесты к этому коду:
package yourpackage
import "testing"
func TestPlus(t *testing.T) {
a, b, expected := 3, 2, 5
got := Plus(a, b)
if got != expected {
t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected)
}
}
func TestMinus(t *testing.T) {
a, b, expected := 3, 2, 1
got := Minus(a, b)
if got != expected {
t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected)
}
}
func TestMul(t *testing.T) {
a, b, expected := 3, 2, 6
got := Mul(a, b)
if got != expected {
t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected)
}
}
При запуске тестов мы видим, помимо ошибки, ещё логирование от других тестов:
time="2021-03-22T22:09:54+03:00" level=info msg="Plus(3, 2)"
time="2021-03-22T22:09:54+03:00" level=info msg="Minus(3, 2)"
time="2021-03-22T22:09:54+03:00" level=info msg="Mul(3, 2)"
--- FAIL: TestMul (0.00s)
yourpackage_test.go:55: Mul(3, 2) return 5; want 6
FAIL
FAIL gotest2/yourpackage 0.002s
FAIL
Если кодовая база большая, то упавшие тесты потеряются среди лишнего логирования. Чтобы такого не было, можно сделать перехват логов в тестах. Для приведённого примера это может выглядеть вот так:
package yourpackage
import (
"io"
"testing"
"github.com/sirupsen/logrus"
)
type logCapturer struct {
*testing.T
origOut io.Writer
}
func (tl logCapturer) Write(p []byte) (n int, err error) {
tl.Logf((string)(p))
return len(p), nil
}
func (tl logCapturer) Release() {
logrus.SetOutput(tl.origOut)
}
func CaptureLog(t *testing.T) *logCapturer {
lc := logCapturer{T: t, origOut: logrus.StandardLogger().Out}
if !testing.Verbose() {
logrus.SetOutput(lc)
}
return &lc
}
func TestPlus(t *testing.T) {
defer CaptureLog(t).Release()
a, b, expected := 3, 2, 5
got := Plus(a, b)
if got != expected {
t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected)
}
}
func TestMinus(t *testing.T) {
defer CaptureLog(t).Release()
a, b, expected := 3, 2, 5
got := Minus(a, b)
if got != expected {
t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected)
}
}
func TestMul(t *testing.T) {
defer CaptureLog(t).Release()
a, b, expected := 3, 2, 5
got := Mul(a, b)
if got != expected {
t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected)
}
}
И тогда вывод тестов будет лаконичнее, и сразу будет понятно, какое логирование и в каком тесте велось:
--- FAIL: TestMul (0.00s)
yourpackage_test.go:16: time="2021-03-22T22:10:52+03:00" level=info msg="Mul(3, 2)"
yourpackage_test.go:55: Mul(3, 2) return 5; want 6
FAIL
FAIL gotest2/yourpackage 0.002s
FAIL
Здесь приведён пример для Logrus, но нечто похожее можно сделать с любой библиотекой логирования. Например, для библиотеки Zap есть отдельный модуль, который облегчает тестирование.Считаем покрытие правильноВ Go всегда была какая-то странность с подсчётом покрытия кода тестами. Сначала нельзя было построить отчёт по покрытию для всех пакетов, написанных в рамках приложения. До сих пор в некоторых репозиториях можно встретить скрипты, похожие на этот, которые используются для запуска тестов по всем пакетам и объединения информации о покрытии в один отчёт. Сейчас с этим всё хорошо, но есть неочевидный момент. Допустим, наш проект состоит из трёх пакетов. И мы хотим для них посчитать покрытие. Обращаемся за помощью к утилите cover, которая скажет нам примерно следующее:
$ go tool cover -help
Usage of 'go tool cover':
Given a coverage profile produced by 'go test':
go test -coverprofile=c.out
...
Display coverage percentages to stdout for each function:
go tool cover -func=c.out
Пробуем:
$ go test -coverprofile=c.out ./...
ok gotestcover/minus 0.001s coverage: 100.0% of statements
? gotestcover/mul [no test files]
ok gotestcover/plus 0.001s coverage: 100.0% of statements
Уже из этого вывода видно, что у нас два пакета покрыты на 100 % и для одного пакета нет тестовых файлов. Получим отчёт о покрытии:
$ go tool cover -func=c.out
gotestcover/minus/minus.go:4: Minus 100.0%
gotestcover/plus/plus.go:4: Plus 100.0%
total: (statements) 100.0%
Но тут что-то не так. В отчёте говорится о полном покрытии тестами. Хотя мы знаем, что это не так. Это всё потому, что при подсчёте покрытия не учитывается пакет, в котором нет тестов. Его не будет и в HTML-отчёте. И кажется, что это не всегда может быть корректным, потому что зачастую мы хотим знать покрытие всего кода, а не только того, для которого мы написали тесты. Это можно исправить, указав специальный параметр при запуске тестов:
go test -coverpkg=./... -coverprofile=c.out ./…
И теперь отчёт выдаёт ожидаемый процент покрытия тестами:
$ go tool cover -func=c.out
gotestcover/minus/minus.go:4: Minus 100.0%
gotestcover/mul/mul.go:4: Mul 0.0%
gotestcover/plus/plus.go:4: Plus 100.0%
total: (statements) 66.7%
Считаем покрытие при тестировании приложения как черного ящикаПисать тесты на Go довольно-таки сложно. И если вы разрабатываете какой-нибудь веб-сервис, то иногда бывает проще написать тесты на другом языке, например, на Python, и тестировать приложение как «чёрный ящик». Но возникает вопрос, а можно ли посчитать покрытие для такого способа тестирования? Да, посчитать можно. В целом, идея довольно проста. Пишем подобный тест:
func TestRunMain(t *testing.T) {
main()
}
Запускаем его, потом интеграционные тесты, и завершаем наш тест. Звучит просто, но есть несколько нюансов. Зачастую надо сделать так, чтобы этот тест не запускался со всеми остальными тестами. Он особый, и для него должна быть отдельная логика запуска. Ещё функция main не должна приводить к выходу с ненулевым кодом возврата. И надо реализовать способ выхода из main по сигналу, не завершая при этом сам тест. То есть в целом надо реализовать для нашего web-сервиса graceful shutdown, что несложно сделать, и это в целом полезно. Давайте на примере реализуем небольшой web-сервис, протестируем его с помощью curl, и посчитаем покрытие тестами. Сервис наш будет выглядеть следующим образом (взято с https://gobyexample.com/http-servers):
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"
)
func hello(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "hello\n")
}
func headers(w http.ResponseWriter, req *http.Request) {
for name, headers := range req.Header {
for _, h := range headers {
fmt.Fprintf(w, "%v: %v\n", name, h)
}
}
}
func main() {
http.HandleFunc("/hello", hello)
http.HandleFunc("/headers", headers)
// Приложим некоторые усилия, чтобы приложение завершилось с нулевым кодом выхода
// Это важно для тестов, и в целом приятно
server := &http.Server{Addr: ":8090", Handler: nil}
// Запускаем приложение в отдельной горутине
go func() {
server.ListenAndServe()
}()
// А в текущей ждём сигнала об остановке приложения
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
server.Shutdown(ctx)
}
И тест к нему:
// +build testrunmain
package main
import "testing"
func TestRunMain(t *testing.T) {
main()
}
Комментарий +build testrunmain говорит о том, что тест будет запускаться только в случае, если передан соответствующий tag. Запускаем наш тест:
$ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out ./...
=== RUN TestRunMain
Тестируем с помощью curl:
$ curl 127.0.0.1:8090/hello
hello
И завершаем наше тестирование, нажав Ctrl+C:
$ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out ./...
=== RUN TestRunMain
^C--- PASS: TestRunMain (100.92s)
PASS
coverage: 80.0% of statements in ./...
ok gobintest 100.926s coverage: 80.0% of statements in ./…
Можем посмотреть покрытие тестами и увидеть, что обработчик headers остался не протестированным:
$ go tool cover -func=c.out
gobintest/main.go:12: hello 100.0%
gobintest/main.go:16: headers 0.0%
gobintest/main.go:24: main 100.0%
total: (statements) 80.0%
Мы рассмотрели некоторые вопросы, которые возникали у нас в компании, когда речь заходила о тестировании приложений на Go. Надеемся, что статья будет вам полезна. Хотите узнать больше о тестировании в Go? Вот ещё несколько интересных статей на хабре: один, два, три.
===========
Источник:
habr.com
===========
Похожие новости:
- [Управление разработкой, Управление проектами] Как работать в команде, которая пишет на 5 языках
- [Учебный процесс в IT, Социальные сети и сообщества] Мы выяснили, что люди копируют со Stack Overflow и насколько часто (перевод)
- [PHP, Тестирование веб-сервисов] Так как же не страдать от функциональных тестов?
- [Управление сообществом, Управление продуктом, Управление медиа, Бизнес-модели, IT-компании] Владельцы онлайн-кинотеатров Wink и More.tv обсуждают слияние своих сервисов
- [Python, PostgreSQL, Django, SQL] SQL в DjangoORM
- [NoSQL, MongoDB, Администрирование баз данных, Хранение данных, Хранилища данных] Шардинг, от которого невозможно отказаться
- [Firefox, Google Chrome, Расширения для браузеров, Браузеры] Почему uBlock Origin лучше работает в Firefox
- [Информационная безопасность, Антивирусная защита, Браузеры, Социальные сети и сообщества] Хакеры создали подставную фирму SecuriElite, чтобы атаковать исследователей безопасности и других хакеров
- [Тестирование IT-систем, Конференции] Где логика?! История тестирования одного микросервиса
- [Разработка веб-сайтов, Управление проектами, Управление медиа] Google и Ростуризм запустили проект «Раскуси Россию» о русской кухне
Теги для поиска: #_testirovanie_itsistem (Тестирование IT-систем), #_go, #_testirovanie_vebservisov (Тестирование веб-сервисов), #_ivi, #_testirovanie (тестирование), #_golang, #_unittesting, #_blog_kompanii_onlajnkinoteatr_ivi (
Блог компании Онлайн-кинотеатр IVI
), #_testirovanie_itsistem (
Тестирование IT-систем
), #_go, #_testirovanie_vebservisov (
Тестирование веб-сервисов
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:57
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В нашей компании в стеке разработки есть язык Go. И иногда, при написании unit-тестов к приложениям написанным на Go, у нас появляются сложности. В этой статье мы расскажем о некоторых моментах, которые мы учитываем при написании тестов. На примерах разберём как их можно использовать. Используем интерфейсы при разработкеОб этом говорится в каждой статье про написании тестов, и эта не будет исключением. Интерфейсы уменьшают связанность между пакетами. Но главное их преимущество при написании тестов заключается в том, что интерфейсы позволяют мокать конкретные зависимости. Например, есть у нас функция, которая на основе содержимого Redis принимает какое-то решение о своей работе: package yourpackage
import ( "context" "github.com/go-redis/redis/v8" ) func CheckLen(ctx context.Context, client *redis.Client, key string) bool { val, err := client.Get(ctx, key).Result() if err != nil { return false } return len(val) < 10 } package yourpackage
import ( "context" "testing" "github.com/go-redis/redis/v8" ) func TestCheckLen(t *testing.T) { ctx := context.Background() rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"}) err := rdb.Set(ctx, "some_key", "value", 0).Err() if err != nil { t.Fatalf("redis return error: %s", err) } got := CheckLen(ctx, rdb, "some_key") if !got { t.Errorf("CheckLen return %v; want true", got) } } package yourpackage
import ( "context" "github.com/go-redis/redis/v8" ) type Storage interface { Set(ctx context.Context, key string, v interface{}) error Get(ctx context.Context, key string) (string, error) } type RedisStorage struct { Redis *redis.Client } func (rs *RedisStorage) Set(ctx context.Context, key string, v interface{}) error { return rs.Redis.Set(ctx, key, v, 0).Err() } func (rs *RedisStorage) Get(ctx context.Context, key string) (string, error) { return rs.Redis.Get(ctx, key).Result() } func CheckLen(ctx context.Context, storage Storage, key string) bool { val, err := storage.Get(ctx, key) if err != nil { return false } return len(val) < 10 } package yourpackage
import ( "context" "testing" ) type testRedis struct{} func (t *testRedis) Get(ctx context.Context, key string) (string, error) { return "value", nil } func (t *testRedis) Set(ctx context.Context, key string, v interface{}) error { return nil } func TestCheckLen(t *testing.T) { ctx := context.Background() storage := &testRedis{} got := CheckLen(ctx, storage, "some_key") if !got { t.Errorf("CheckLen return %v; want true", got) } } mockery --recursive=true --inpackage --name=Storage
package yourpackage
import ( "context" "testing" mock "github.com/stretchr/testify/mock" ) func TestCheckLen(t *testing.T) { ctx := context.Background() storage := new(MockStorage) storage.On("Get", mock.Anything, "some_key").Return("value", nil) got := CheckLen(ctx, storage, "some_key") if !got { t.Errorf("CheckLen return %v; want true", got) } package yourpackage
import ( log "github.com/sirupsen/logrus" ) func Minus(a, b int) int { log.Infof("Minus(%v, %v)", a, b) return a - b } func Plus(a, b int) int { log.Infof("Plus(%v, %v)", a, b) return a + b } func Mul(a, b int) int { log.Infof("Mul(%v, %v)", a, b) return a + b // тут ошибка } package yourpackage
import "testing" func TestPlus(t *testing.T) { a, b, expected := 3, 2, 5 got := Plus(a, b) if got != expected { t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected) } } func TestMinus(t *testing.T) { a, b, expected := 3, 2, 1 got := Minus(a, b) if got != expected { t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected) } } func TestMul(t *testing.T) { a, b, expected := 3, 2, 6 got := Mul(a, b) if got != expected { t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected) } } time="2021-03-22T22:09:54+03:00" level=info msg="Plus(3, 2)"
time="2021-03-22T22:09:54+03:00" level=info msg="Minus(3, 2)" time="2021-03-22T22:09:54+03:00" level=info msg="Mul(3, 2)" --- FAIL: TestMul (0.00s) yourpackage_test.go:55: Mul(3, 2) return 5; want 6 FAIL FAIL gotest2/yourpackage 0.002s FAIL package yourpackage
import ( "io" "testing" "github.com/sirupsen/logrus" ) type logCapturer struct { *testing.T origOut io.Writer } func (tl logCapturer) Write(p []byte) (n int, err error) { tl.Logf((string)(p)) return len(p), nil } func (tl logCapturer) Release() { logrus.SetOutput(tl.origOut) } func CaptureLog(t *testing.T) *logCapturer { lc := logCapturer{T: t, origOut: logrus.StandardLogger().Out} if !testing.Verbose() { logrus.SetOutput(lc) } return &lc } func TestPlus(t *testing.T) { defer CaptureLog(t).Release() a, b, expected := 3, 2, 5 got := Plus(a, b) if got != expected { t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected) } } func TestMinus(t *testing.T) { defer CaptureLog(t).Release() a, b, expected := 3, 2, 5 got := Minus(a, b) if got != expected { t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected) } } func TestMul(t *testing.T) { defer CaptureLog(t).Release() a, b, expected := 3, 2, 5 got := Mul(a, b) if got != expected { t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected) } } --- FAIL: TestMul (0.00s)
yourpackage_test.go:16: time="2021-03-22T22:10:52+03:00" level=info msg="Mul(3, 2)" yourpackage_test.go:55: Mul(3, 2) return 5; want 6 FAIL FAIL gotest2/yourpackage 0.002s FAIL $ go tool cover -help
Usage of 'go tool cover': Given a coverage profile produced by 'go test': go test -coverprofile=c.out ... Display coverage percentages to stdout for each function: go tool cover -func=c.out $ go test -coverprofile=c.out ./...
ok gotestcover/minus 0.001s coverage: 100.0% of statements ? gotestcover/mul [no test files] ok gotestcover/plus 0.001s coverage: 100.0% of statements $ go tool cover -func=c.out
gotestcover/minus/minus.go:4: Minus 100.0% gotestcover/plus/plus.go:4: Plus 100.0% total: (statements) 100.0% go test -coverpkg=./... -coverprofile=c.out ./…
$ go tool cover -func=c.out
gotestcover/minus/minus.go:4: Minus 100.0% gotestcover/mul/mul.go:4: Mul 0.0% gotestcover/plus/plus.go:4: Plus 100.0% total: (statements) 66.7% func TestRunMain(t *testing.T) {
main() } package main
import ( "context" "fmt" "net/http" "os" "os/signal" "time" ) func hello(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "hello\n") } func headers(w http.ResponseWriter, req *http.Request) { for name, headers := range req.Header { for _, h := range headers { fmt.Fprintf(w, "%v: %v\n", name, h) } } } func main() { http.HandleFunc("/hello", hello) http.HandleFunc("/headers", headers) // Приложим некоторые усилия, чтобы приложение завершилось с нулевым кодом выхода // Это важно для тестов, и в целом приятно server := &http.Server{Addr: ":8090", Handler: nil} // Запускаем приложение в отдельной горутине go func() { server.ListenAndServe() }() // А в текущей ждём сигнала об остановке приложения quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) <-quit ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() server.Shutdown(ctx) } // +build testrunmain
package main import "testing" func TestRunMain(t *testing.T) { main() } $ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out ./...
=== RUN TestRunMain $ curl 127.0.0.1:8090/hello
hello $ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out ./...
=== RUN TestRunMain ^C--- PASS: TestRunMain (100.92s) PASS coverage: 80.0% of statements in ./... ok gobintest 100.926s coverage: 80.0% of statements in ./… $ go tool cover -func=c.out
gobintest/main.go:12: hello 100.0% gobintest/main.go:16: headers 0.0% gobintest/main.go:24: main 100.0% total: (statements) 80.0% =========== Источник: habr.com =========== Похожие новости:
Блог компании Онлайн-кинотеатр IVI ), #_testirovanie_itsistem ( Тестирование IT-систем ), #_go, #_testirovanie_vebservisov ( Тестирование веб-сервисов ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:57
Часовой пояс: UTC + 5